Merge branch stable-3.5
* stable-3.5:
Fix LoopOverCharArray bug pattern flagged by error prone
Fix FloggerStringConcatenation bug pattern flagged by error prone
docs/config-gerrit: fix asciidoc link markup
Detect DelegateRepository in GarbageCollection operation
docs: fix its vs it's grammar
MergeUtil: Fix invalid argument index specifier flagged by JDK 17
Bazel: Add no_rbe tag to //tools/bzl:always_pass_test
Add possibility to override MultiBaseLocalDiskRepositoryManager
Change-Id: I74a341cc92e5fc862bddc6f6d6e90c096bed1c74
diff --git a/.bazelrc b/.bazelrc
index b4eafb1..db1fd57 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -14,7 +14,7 @@
build --announce_rc
test --build_tests_only
-test --test_output=errors
+test --test_output=all
test --java_toolchain=//tools:error_prone_warnings_toolchain_java11
import %workspace%/tools/remote-bazelrc
diff --git a/.gitignore b/.gitignore
index 95f94ba..8edc10e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
# Keep following lines sorted according to `LC_COLLATE=C sort`
+*.code-workspace
*.eml
*.iml
*.log
diff --git a/BUILD b/BUILD
index 084d383..f6ec40e 100644
--- a/BUILD
+++ b/BUILD
@@ -20,8 +20,8 @@
genrule(
name = "gen_version",
outs = ["version.txt"],
- cmd = ("cat bazel-out/volatile-status.txt bazel-out/stable-status.txt | " +
- "grep STABLE_BUILD_GERRIT_LABEL | cut -d ' ' -f 2 > $@"),
+ cmd = ("(cat bazel-out/volatile-status.txt bazel-out/stable-status.txt | " +
+ "grep STABLE_BUILD_GERRIT_LABEL | cut -d ' ' -f 2) > $@ || echo 'UNKNOWN' > $@"),
stamp = 1,
)
diff --git a/Documentation/concept-patch-sets.txt b/Documentation/concept-patch-sets.txt
index 274fbb0..e39d091 100644
--- a/Documentation/concept-patch-sets.txt
+++ b/Documentation/concept-patch-sets.txt
@@ -88,8 +88,8 @@
evolves, such as "Added more unit tests." Unlike the change description, a patch
set description does not become a part of the project's history.
-To add a patch set description, click *Add a patch set description*, located in
-the file list, or provide it link:user-upload.html#patch_set_description[on upload].
+To add a patch set description provide it
+link:user-upload.html#patch_set_description[on upload].
GERRIT
------
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 22c7e9f..5418555 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -954,12 +954,6 @@
+
If direct updates are made to `All-Users`, this cache should be flushed.
-cache `"approvals"`::
-+
-Cache entries contain approvals for a given patch set. This includes
-approvals granted on this patch set as well as approvals copied from
-earlier patch sets.
-
cache `"adv_bases"`::
+
Used only for push over smart HTTP when branch level access controls
@@ -1205,6 +1199,14 @@
Result of checking if one change or commit is a pure/clean revert of
another.
+cache `"soy_sauce_compiled_templates"`::
++
+Caches compiled soy templates. Stores at most only one key-value pair with
+a constant key value and the value is a compiled SoySauce templates. The value
+is reloaded automatically every few seconds if there are reads from the cache.
+If cache is not used for 1 minute, the item is removed (i.e. emails can be send
+with templates which are max 1 minute old).
+
cache `"sshkeys"`::
+
Caches unpacked versions of user SSH keys, so the internal SSH daemon
@@ -4851,16 +4853,6 @@
replicate = replication start
----
-[[ssh]]
-=== Section ssh
-
-[[ssh.clientImplementation]]ssh.clientImplementation::
-+
-JCraft JSch client is supported in addition to Apache MINA SSH client.
-To use JSch client set the value to `JSCH`.
-+
-By default, `APACHE`.
-
[[sshd]]
=== Section sshd
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index 5889c75..f6d52e8 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -97,7 +97,7 @@
value = -1 Fails
value = 0 No score
value = +1 Verified
- copyAllScoresIfNoCodeChange = true
+ copyCondition = changekind:NO_CODE_CHANGE
----
The range of values is:
@@ -176,6 +176,11 @@
The name for a label, consisting only of alphanumeric characters and
`-`.
+[[label_description]]
+=== `label.Label-Name.description`
+
+The label description. This field can provide extra information of what the
+label is supposed to do.
[[label_value]]
=== `label.Label-Name.value`
@@ -265,6 +270,9 @@
[[label_copyAnyScore]]
=== `label.Label-Name.copyAnyScore`
+*DEPRECATED: use `is:ANY` predicate in
+link:config-labels.html#label_copyCondition[copyCondition] instead*
+
If true, any score for the label is copied forward when a new patch
set is uploaded. Defaults to false.
@@ -323,6 +331,9 @@
[[label_copyMinScore]]
=== `label.Label-Name.copyMinScore`
+*DEPRECATED: use `is:MIN` predicate in
+link:config-labels.html#label_copyCondition[copyCondition] instead*
+
If true, the lowest possible negative value for the label is copied
forward when a new patch set is uploaded. Defaults to false, except
for All-Projects which has it true by default.
@@ -330,6 +341,9 @@
[[label_copyMaxScore]]
=== `label.Label-Name.copyMaxScore`
+*DEPRECATED: use `is:MAX` predicate in
+link:config-labels.html#label_copyCondition[copyCondition] instead*
+
If true, the highest possible positive value for the label is copied
forward when a new patch set is uploaded. This can be used to enable
sticky approvals, reducing turn-around for trivial cleanups prior to
@@ -338,6 +352,9 @@
[[label_copyAllScoresIfListOfFilesDidNotChange]]
=== `label.Label-Name.copyAllScoresIfListOfFilesDidNotChange`
+*DEPRECATED: use `is:ANY AND has:unchanged-files` predicates in
+link:config-labels.html#label_copyCondition[copyCondition] instead*
+
This policy is useful if you don't want to trigger CI or human
verification again if the list of files didn't change.
@@ -354,6 +371,9 @@
[[label_copyAllScoresOnMergeFirstParentUpdate]]
=== `label.Label-Name.copyAllScoresOnMergeFirstParentUpdate`
+*DEPRECATED: use `is:ANY AND changekind:MERGE_FIRST_PARENT_UPDATE` predicates
+in link:config-labels.html#label_copyCondition[copyCondition] instead*
+
This policy is useful if you don't want to trigger CI or human
verification again if your target branch moved on but the feature
branch being merged into the target branch did not change. It only
@@ -371,6 +391,9 @@
[[label_copyAllScoresOnTrivialRebase]]
=== `label.Label-Name.copyAllScoresOnTrivialRebase`
+*DEPRECATED: use `is:ANY AND changekind:TRIVIAL_REBASE` predicates
+in link:config-labels.html#label_copyCondition[copyCondition] instead*
+
If true, all scores for the label are copied forward when a new patch set is
uploaded that is a trivial rebase. A new patch set is considered to be trivial
rebase if the commit message is the same as in the previous patch set and if it
@@ -388,6 +411,9 @@
[[label_copyAllScoresIfNoCodeChange]]
=== `label.Label-Name.copyAllScoresIfNoCodeChange`
+*DEPRECATED: use `is:ANY AND changekind:NO_CODE_CHANGE` predicates in
+link:config-labels.html#label_copyCondition[copyCondition] instead*
+
If true, all scores for the label are copied forward when a new patch set is
uploaded that has the same parent tree as the previous patch set and the same
code diff (including context lines) as the previous patch set. This means only
@@ -402,6 +428,9 @@
[[label_copyAllScoresIfNoChange]]
=== `label.Label-Name.copyAllScoresIfNoChange`
+*DEPRECATED: use `is:ANY AND changekind:NO_CHANGE` predicates in
+link:config-labels.html#label_copyCondition[copyCondition] instead*
+
If true, all scores for the label are copied forward when a new patch
set is uploaded that has the same parent tree, code delta, and commit
message as the previous patch set. This means that only the patch
diff --git a/Documentation/config-themes.txt b/Documentation/config-themes.txt
index a83c747..73fb1bc 100644
--- a/Documentation/config-themes.txt
+++ b/Documentation/config-themes.txt
@@ -4,11 +4,12 @@
the browser, allowing organizations to alter the look and
feel of the application to fit with their general scheme.
-== HTML Header/Footer and CSS
+== HTML Header/Footer and CSS for login screens
-The HTML header, footer and CSS may be customized for login
-screens (LDAP, OAuth, OpenId) and the internally managed
-Gitweb servlet.
+The HTML header, footer, and CSS may be customized for login screens (LDAP,
+OAuth, OpenId) and the internally managed Gitweb servlet. See
+link:pg-plugin-dev.txt[JavaScript Plugin Development and API] for documentation
+on modifying styles for the rest of Gerrit (not login screens).
At startup Gerrit reads the following files (if they exist) and
uses them to customize the HTML page it sends to clients:
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index c982a43..c5c2dc0 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -18,7 +18,7 @@
To build Gerrit from source, you need:
* A Linux or macOS system (Windows is not supported at this time)
-* A JDK for Java 8|11|...
+* A JDK for Java 11|...
* Python 3
* link:https://github.com/nodesource/distributions/blob/master/README.md[Node.js (including npm),role=external,window=_blank]
* Bower (`npm install -g bower`)
@@ -48,16 +48,6 @@
`java -version`
-[[java-8]]
-==== Java 8 support (deprecated)
-
-Java 8 is a legacy Java release and support for Java 8 will be discontinued
-in future gerrit releases. To build Gerrit with Java 8 language level, run:
-
-```
- $ bazel build --java_toolchain //tools:error_prone_warnings_toolchain :release
-```
-
[[java-11]]
==== Java 11 support
@@ -325,18 +315,6 @@
bazel test //javatests/com/google/gerrit/acceptance/rest/account:rest_account
----
-To run SSH tests using JSch ssh client:
-
-----
- bazel test --test_env=SSH_CLIENT_IMPLEMENTATION=JSCH //...
-----
-
-To run SSH tests using Apache MINA ssh client:
-
-----
- bazel test --test_env=SSH_CLIENT_IMPLEMENTATION=APACHE //...
-----
-
To run only tests that do not use SSH:
----
diff --git a/Documentation/dev-e2e-tests.txt b/Documentation/dev-e2e-tests.txt
index c50a293..802b484 100644
--- a/Documentation/dev-e2e-tests.txt
+++ b/Documentation/dev-e2e-tests.txt
@@ -102,8 +102,7 @@
=== SSH keys
If you are running SSH commands, the private keys of the users used for testing need to go in
-`/tmp/ssh-keys`. The keys need to be generated this way (JSch won't validate them
-link:https://stackoverflow.com/questions/53134212/invalid-privatekey-when-using-jsch[otherwise,role=external,window=_blank]):
+`/tmp/ssh-keys`. The keys need to be generated this way and won't be validated.
----
mkdir /tmp/ssh-keys
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index e18d7b0..dce5eb0 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -31,9 +31,6 @@
----
First, generate the Eclipse project by running the `tools/eclipse/project.py` script.
-If running Eclipse on Java 8, add the extra parameter
-`-e='--java_toolchain=//tools:error_prone_warnings_toolchain'`
-for generating a compatible project.
Then, in Eclipse, choose 'Import existing project' and select the `gerrit` project
from the current working directory.
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index c11a3fa..38ce7b3 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2768,6 +2768,23 @@
prevent callers using ETags from potentially seeing outdated submittability
information.
+`SubmitRule` interface will soon deprecated. Instead, a global `SubmitRequirement`
+can be bound by plugin.
+
+[source, java]
+----
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.inject.AbstractModule;
+
+public class MyPluginModule extends AbstractModule {
+ @Override
+ protected void configure() {
+ bind(SubmitRequirement.class).annotatedWith(Exports.named("myPlugin"))
+ .toInstance(myPluginSubmitRequirement);
+ }
+}
+----
+
[[change-etag-computation]]
== Change ETag Computation
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 3f23385..8fc3852 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -662,6 +662,10 @@
* If you have a series of private changes and share one with reviewers,
the reviewers can also see the commits of the predecessor private
changes through the commit parent relationship.
+* Users who would have permission to access the change except for its
+ private status and knowledge of its commit ID (e.g. through CI logs
+ or build artifacts containing build numbers) can fetch the code
+ using the commit ID.
[[ignore]]
== Ignoring Or Marking Changes As 'Reviewed'
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 685e73b..490e141 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -32,7 +32,7 @@
Gerrit includes an SSH daemon (Apache SSHD), to support authenticated
uploads of changes directly from `git push` command line clients.
-Gerrit includes an SSH client (JSch), to support authenticated
+Gerrit includes an SSH client (Apache SSHD), to support authenticated
replication of changes to remote systems, such as for automatic
updates of mirror servers, or realtime backups.
@@ -2375,43 +2375,6 @@
----
-[[jsch]]
-jsch
-
-* jsch
-
-[[jsch_license]]
-----
-Copyright (c) 2002-2012 Atsuhiko Yamanaka, JCraft,Inc.
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
- 1. Redistributions of source code must retain the above copyright notice,
- this list of conditions and the following disclaimer.
-
- 2. Redistributions in binary form must reproduce the above copyright
- notice, this list of conditions and the following disclaimer in
- the documentation and/or other materials provided with the distribution.
-
- 3. The names of the authors may not be used to endorse or promote products
- derived from this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
-INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
-FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
-INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
-INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
-OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
-LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
-NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
-EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-----
-
-
[[jsoup]]
jsoup
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 0318cd7..d8b6250 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -113,7 +113,7 @@
* `receivecommits/ps_revision_missing`: errors due to patch set revision missing
* `receivecommits/push_count`: number of pushes
** `kind`:
- The push kind (direct vs. magic).
+ The push kind (magic, direct or direct_submit).
** `project`:
The name of the project for which the push is done.
** `type`:
diff --git a/Documentation/pg-plugin-endpoints.txt b/Documentation/pg-plugin-endpoints.txt
index c16d0d4..f2a72f1 100644
--- a/Documentation/pg-plugin-endpoints.txt
+++ b/Documentation/pg-plugin-endpoints.txt
@@ -228,3 +228,13 @@
+
current revision displayed, an instance of
link:rest-api-changes.html#revision-info[RevisionInfo]
+
+=== account-status-icon
+The `account-status-icon` extension point adds an icon to all account chips and
+labels.
+
+In addition to default parameters, the following are available:
+
+* `accountId`
++
+the Id of the account that the status icon should correspond to.
\ No newline at end of file
diff --git a/Documentation/pgm-daemon.txt b/Documentation/pgm-daemon.txt
index cf6560b..586f685 100644
--- a/Documentation/pgm-daemon.txt
+++ b/Documentation/pgm-daemon.txt
@@ -61,7 +61,7 @@
Run init before starting the daemon. This will create a new site or
upgrade an existing site.
---s::
+-s::
Start link:dev-inspector.html[Gerrit Inspector] on the console, a
built-in interactive inspection environment to assist debugging and
troubleshooting of Gerrit code.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 0a203cc..88ddc22 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -1857,7 +1857,8 @@
* The given change.
* If link:config-gerrit.html#change.submitWholeTopic[`change.submitWholeTopic`]
- is enabled, include all open changes with the same topic.
+ is enabled OR if the `o=TOPIC_CLOSURE` query parameter is passed, include all
+ open changes with the same topic.
* For each change whose submit type is not CHERRY_PICK, include unmerged
ancestors targeting the same branch.
@@ -1884,7 +1885,7 @@
Standard link:#query-options[formatting options] can be specified
with the `o` parameter, as well as the `submitted_together` specific
-option `NON_VISIBLE_CHANGES`.
+options `NON_VISIBLE_CHANGES` and `TOPIC_CLOSURE`.
.Response
----
@@ -4581,60 +4582,6 @@
If the `path` parameter is set, the returned content is a diff of the single
file that the path refers to.
-[[submit-preview]]
-=== Submit Preview
---
-'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/preview_submit'
---
-Gets a file containing thin bundles of all modified projects if this
-change was submitted. The bundles are named `${ProjectName}.git`.
-Each thin bundle contains enough to construct the state in which a project would
-be in if this change were submitted. The base of the thin bundles are the
-current target branches, so to make use of this call in a non-racy way, first
-get the bundles and then fetch all projects contained in the bundle.
-(This assumes no non-fastforward pushes).
-
-You need to give a parameter '?format=zip' or '?format=tar' to specify the
-format for the outer container. It is always possible to use tgz, even if
-tgz is not in the list of allowed archive formats.
-
-To make good use of this call, you would roughly need code as found at:
-----
- $ curl -Lo preview_submit_test.sh http://review.example.com:8080/tools/scripts/preview_submit_test.sh
-----
-.Request
-----
- GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/preview_submit?zip HTTP/1.0
-----
-
-.Response
-----
- HTTP/1.1 200 OK
- Date: Tue, 13 Sep 2016 19:13:46 GMT
- Content-Disposition: attachment; filename="submit-preview-147.zip"
- X-Content-Type-Options: nosniff
- Cache-Control: no-cache, no-store, max-age=0, must-revalidate
- Pragma: no-cache
- Expires: Mon, 01 Jan 1990 00:00:00 GMT
- Content-Type: application/x-zip
- Transfer-Encoding: chunked
-
- [binary stuff]
-----
-
-In case of an error, the response is not a zip file but a regular json response,
-containing only the error message:
-
-.Response
-----
- HTTP/1.1 200 OK
- Content-Disposition: attachment
- Content-Type: application/json; charset=UTF-8
-
- )]}'
- "Anonymous users cannot submit"
-----
-
[[get-mergeable]]
=== Get Mergeable
--
@@ -5084,6 +5031,9 @@
with a new message, which contains the name of the user who deletes
the comment and the reason why it's deleted.
+This endpoint also marks the comment as resolved since deleted comments are not
+actionable.
+
Note that only users with the
link:access-control.html#capability_administrateServer[Administrate Server]
global capability are permitted to delete a comment.
@@ -6520,7 +6470,14 @@
|`topic` |optional|The topic to which this change belongs.
|`attention_set` |optional|
The map that maps link:rest-api-accounts.html#account-id[account IDs]
-to link:#attention-set-info[AttentionSetInfo] of that account.
+to link:#attention-set-info[AttentionSetInfo] of that account. Those are all
+accounts that are currently in the attention set.
+|`removed_from_attention_set` |optional|
+The map that maps link:rest-api-accounts.html#account-id[account IDs]
+to link:#attention-set-info[AttentionSetInfo] of that account. Those are all
+accounts that were in the attention set but were removed. The
+link:#attention-set-info[AttentionSetInfo] is the latest and most recent removal
+ of the account from the attention set.
|`assignee` |optional|
The assignee of the change as an link:rest-api-accounts.html#account-info[
AccountInfo] entity.
@@ -7104,6 +7061,9 @@
Additional information about whom to notify about the update as a map
of link:user-notify.html#recipient-types[recipient type] to
link:#notify-info[NotifyInfo] entity.
+|`ignore_automatic_attention_set_rules`|optional|
+If set to true, ignore all automatic attention set rules described in the
+link:#attention-set[attention set]. When not set, the default is false.
|=============================
[[description-input]]
@@ -7436,6 +7396,7 @@
Whether the label is optional. Optional means the label may be set, but
it's neither necessary for submission nor does it block submission if
set.
+|`description` |optional| The description of the label.
|===========================
==== Fields set by `LABELS`
@@ -8263,21 +8224,24 @@
The `SubmitRequirementExpressionInfo` describes the result of evaluating a
single submit requirement expression, for example `label:code-review=+2`.
-[options="header",cols="1,6"]
+[options="header",cols="1,^1,5"]
|===========================
-|Field Name |Description
-|`expression`|
+|Field Name ||Description
+|`expression`|optional|
The submit requirement expression as a string, for example
`branch:refs/heads/foo and label:verified=+1`.
-|`fulfilled`|
+|`fulfilled`||
True if the submit requirement is fulfilled for the change.
-|`passing_atoms`|
+|`passing_atoms`|optional|
A list of passing atoms as strings. For the above expression,
`passing_atoms` can contain ["branch:refs/heads/foo"] if the branch predicate is
fulfilled for the change.
-|`failing_atoms`|
+|`failing_atoms`|optional|
A list of failing atoms. This is similar to `passing_atoms` except that it
contains the list of predicates that are not fulfilled for the change.
+|`error_message`|optional|
+If the submit requirement fails during evaluation, this string will contain
+an error message describing why it failed.
|===========================
[[submit-requirement-input]]
@@ -8320,7 +8284,8 @@
Description of the submit requirement.
|`status`||
Status describing the result of evaluating the submit requirement. The status
-is one of (`SATISFIED`, `UNSATISFIED`, `OVERRIDDEN`, `NOT_APPLICABLE`, `ERROR`).
+is one of (`SATISFIED`, `UNSATISFIED`, `OVERRIDDEN`, `NOT_APPLICABLE`, `ERROR`,
+`FORCED`).
|`is_legacy`||
If true, this submit requirement result was created from a legacy
link:#submit-record[SubmitRecord]. Otherwise, it was created by evaluating a
@@ -8329,6 +8294,8 @@
A link:#submit-requirement-expression-info[SubmitRequirementExpressionInfo]
containing the result of evaluating the applicability expression. Not set if the
submit requirement did not define an applicability expression.
+Note that fields `expression`, `passing_atoms` and `failing_atoms` are always
+omitted for the `applicability_expression_result`.
|`submittability_expression_result`||
A link:#submit-requirement-expression-info[SubmitRequirementExpressionInfo]
containing the result of evaluating the submittability expression.
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 6ddc8bd..0b01660 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -3986,6 +3986,8 @@
|Field Name ||Description
|`name` ||
The link:config-labels.html#label_name[name] of the label.
+|`description` |optional|
+The description of the label.
|`project_name` ||
The name of the project in which this label is defined.
|`function` ||
@@ -4006,30 +4008,32 @@
Whether this label can be link:config-labels.html#label_canOverride[overridden]
by child projects.
|`copy_any_score`|`false` if not set|
-Whether link:config-labels.html#label_copyAnyScore[copyAnyScore] is set on the
-label.
+*DEPRECATED* Whether link:config-labels.html#label_copyAnyScore[copyAnyScore]
+is set on the label.
|`copy_condition`|optional|
See link:config-labels.html#label_copyCondition[copyCondition].
|`copy_min_score`|`false` if not set|
-Whether link:config-labels.html#label_copyMinScore[copyMinScore] is set on the
-label.
+*DEPRECATED* Whether link:config-labels.html#label_copyMinScore[copyMinScore]
+is set on the label.
|`copy_max_score`|`false` if not set|
-Whether link:config-labels.html#label_copyMaxScore[copyMaxScore] is set on the
-label.
+*DEPRECATED* Whether link:config-labels.html#label_copyMaxScore[copyMaxScore]
+is set on the label.
|`copy_all_scores_if_no_change`|`false` if not set|
-Whether link:config-labels.html#label_copyAllScoresIfNoChange[
+*DEPRECATED* Whether link:config-labels.html#label_copyAllScoresIfNoChange[
copyAllScoresIfNoChange] is set on the label.
|`copy_all_scores_if_no_code_change`|`false` if not set|
-Whether link:config-labels.html#label_copyAllScoresIfNoCodeChange[
+*DEPRECATED* Whether link:config-labels.html#label_copyAllScoresIfNoCodeChange[
copyAllScoresIfNoCodeChange] is set on the label.
|`copy_all_scores_on_trivial_rebase`|`false` if not set|
-Whether link:config-labels.html#label_copyAllScoresOnTrivialRebase[
+*DEPRECATED* Whether link:config-labels.html#label_copyAllScoresOnTrivialRebase[
copyAllScoresOnTrivialRebase] is set on the label.
|`copy_all_scores_if_list_of_files_did_not_change`|`false` if not set|
-Whether link:config-labels.html#label_copyAllScoresIfListOfFilesDidNotChange[
+*DEPRECATED* Whether
+link:config-labels.html#label_copyAllScoresIfListOfFilesDidNotChange[
copyAllScoresIfListOfFilesDidNotChange] is set on the label.
|`copy_all_scores_on_merge_first_parent_update`|`false` if not set|
-Whether link:config-labels.html#label_copyAllScoresOnMergeFirstParentUpdate[
+*DEPRECATED* Whether
+link:config-labels.html#label_copyAllScoresOnMergeFirstParentUpdate[
copyAllScoresOnMergeFirstParentUpdate] is set on the label.
|`copy_values` |optional|
List of values that should be copied forward when a new patch set is uploaded.
@@ -4059,6 +4063,8 @@
For label creation the name is required if this `LabelDefinitionInput` entity
is contained in a link:#batch-label-input[BatchLabelInput]
entity.
+|`description` |optional|
+The new description for the label.
|`function` |optional|
The new link:config-labels.html#label_function[function] of the label (can be
`MaxWithBlock`, `AnyWithBlock`, `MaxNoBlock`, `NoBlock`, `NoOp` and `PatchSetLock`.
diff --git a/Documentation/user-notify.txt b/Documentation/user-notify.txt
index 128bae6..20ad07c 100644
--- a/Documentation/user-notify.txt
+++ b/Documentation/user-notify.txt
@@ -11,9 +11,9 @@
Those are the available recipient types:
-* `to`: The standard To field is used; addresses are visible to all.
-* `cc`: The standard CC field is used; addresses are visible to all.
-* `bcc`: SMTP RCPT TO is used to hide the address.
+* `TO`: The standard To field is used; addresses are visible to all.
+* `CC`: The standard CC field is used; addresses are visible to all.
+* `BCC`: SMTP RCPT TO is used to hide the address.
[[user]]
== User Level Settings
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index cc8d813..a24d80d 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -264,6 +264,11 @@
often combined with 'branch:' and 'project:' operators to select
all related changes in a series.
+[[prefixtopic]]
+prefixtopic:'TOPIC'::
++
+Changes whose designated topic start with 'TOPIC'.
+
[[inhashtag]]
inhashtag:'HASHTAG'::
+
@@ -280,6 +285,12 @@
Changes whose link:intro-user.html#hashtags[hashtag] matches 'HASHTAG'.
The match is case-insensitive.
+[[prefixhashtag]]
+prefixhashtag:'HASHTAG'::
++
+Changes whose link:intro-user.html#hashtags[hashtag] start with 'HASHTAG'.
+The match is case-insensitive.
+
[[cherrypickof]]
cherrypickof:'CHANGE[,PATCHSET]'::
+
@@ -565,6 +576,11 @@
cherry-picked locally using the git cherry-pick command and then
pushed to Gerrit.
+[[pure-revert]]
+is:pure-revert::
++
+True if the change is a pure revert.
+
[[status]]
status:open, status:pending, status:new::
+
@@ -634,16 +650,6 @@
email address. The special case of `committer:self` will find changes committed
by the caller.
-
-[[submittable]]
-submittable:'SUBMIT_STATUS'::
-+
-Changes having the given submit record status after applying submit
-rules. Valid statuses are in the `status` field of
-link:rest-api-changes.html#submit-record[SubmitRecord]. This operator
-only applies to the top-level status; individual label statuses can be
-searched link:#labels[by label].
-
[[rule]]
rule:'SUBMIT_RULE_NAME'::
+
@@ -772,6 +778,22 @@
+
Matches changes with label voted with any score.
+`label:Code-Review=+1,count=2`::
++
+Matches changes with exactly two +1 votes to the code-review label. The {MAX,
+MIN, ANY} votes can also be used, for example `label:Code-Review=MAX,count=2` is
+equivalent to `label:Code-Review=2,count=2` (if 2 is the maximum positive vote
+for the code review label). The maximum supported value for `count` is 5.
+`count=0` is not allowed and the query request will fail with `400 Bad Request`.
+
+`label:Code-Review=+1,count>=2`::
++
+Matches changes having two or more +1 votes to the code-review label. Can also
+be used with the {MAX, MIN, ANY} label votes. All operators `>`, `>=`, `<`, `<=`
+are supported.
+Note that a query like `label:Code-Review=+1,count<x` will not match with
+changes having zero +1 votes to this label.
+
`label:Non-Author-Code-Review=need`::
+
Matches changes where the submit rules indicate that a label named
diff --git a/WORKSPACE b/WORKSPACE
index 37f30b1..7ff4c0d 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -31,6 +31,7 @@
load("//tools/bzl:maven_jar.bzl", "GERRIT", "MAVEN_LOCAL", "maven_jar")
load("//plugins:external_plugin_deps.bzl", "external_plugin_deps")
load("//tools:nongoogle.bzl", "declare_nongoogle_deps")
+load("//tools:deps.bzl", "CAFFEINE_VERS", "java_dependencies")
http_archive(
name = "rbe_jdk11",
@@ -116,90 +117,7 @@
path = "modules/jgit",
)
-ANTLR_VERS = "3.5.2"
-
-maven_jar(
- name = "java-runtime",
- artifact = "org.antlr:antlr-runtime:" + ANTLR_VERS,
- sha1 = "cd9cd41361c155f3af0f653009dcecb08d8b4afd",
-)
-
-maven_jar(
- name = "stringtemplate",
- artifact = "org.antlr:stringtemplate:4.0.2",
- sha1 = "e28e09e2d44d60506a7bcb004d6c23ff35c6ac08",
-)
-
-maven_jar(
- name = "org-antlr",
- artifact = "org.antlr:antlr:" + ANTLR_VERS,
- sha1 = "c4a65c950bfc3e7d04309c515b2177c00baf7764",
-)
-
-maven_jar(
- name = "antlr27",
- artifact = "antlr:antlr:2.7.7",
- attach_source = False,
- sha1 = "83cd2cd674a217ade95a4bb83a8a14f351f48bd0",
-)
-
-maven_jar(
- name = "aopalliance",
- artifact = "aopalliance:aopalliance:1.0",
- sha1 = "0235ba8b489512805ac13a8f9ea77a1ca5ebe3e8",
-)
-
-maven_jar(
- name = "javax_inject",
- artifact = "javax.inject:javax.inject:1",
- sha1 = "6975da39a7040257bd51d21a231b76c915872d38",
-)
-
-maven_jar(
- name = "servlet-api",
- artifact = "org.apache.tomcat:tomcat-servlet-api:8.5.23",
- sha1 = "021a212688ec94fe77aff74ab34cc74f6f940e60",
-)
-
-# JGit's transitive dependencies
-maven_jar(
- name = "hamcrest-library",
- artifact = "org.hamcrest:hamcrest-library:1.3",
- sha1 = "4785a3c21320980282f9f33d0d1264a69040538f",
-)
-
-maven_jar(
- name = "jzlib",
- artifact = "com.jcraft:jzlib:1.1.1",
- sha1 = "a1551373315ffc2f96130a0e5704f74e151777ba",
-)
-
-maven_jar(
- name = "javaewah",
- artifact = "com.googlecode.javaewah:JavaEWAH:1.1.6",
- attach_source = False,
- sha1 = "94ad16d728b374d65bd897625f3fbb3da223a2b6",
-)
-
-maven_jar(
- name = "error-prone-annotations",
- artifact = "com.google.errorprone:error_prone_annotations:2.3.3",
- sha1 = "42aa5155a54a87d70af32d4b0d06bf43779de0e2",
-)
-
-maven_jar(
- name = "gson",
- artifact = "com.google.code.gson:gson:2.8.5",
- sha1 = "f645ed69d595b24d4cf8b3fbb64cc505bede8829",
-)
-
-CAFFEINE_VERS = "2.8.5"
-
-maven_jar(
- name = "caffeine",
- artifact = "com.github.ben-manes.caffeine:caffeine:" + CAFFEINE_VERS,
- sha1 = "f0eafef6e1529a44e36549cd9d1fc06d3a57f384",
-)
+java_dependencies()
CAFFEINE_GUAVA_SHA256 = "a7ce6d29c40bccd688815a6734070c55b20cd326351a06886a6144005aa32299"
@@ -221,688 +139,8 @@
],
)
-maven_jar(
- name = "guava-failureaccess",
- artifact = "com.google.guava:failureaccess:1.0.1",
- sha1 = "1dcf1de382a0bf95a3d8b0849546c88bac1292c9",
-)
-
-maven_jar(
- name = "jsch",
- artifact = "com.jcraft:jsch:0.1.54",
- sha1 = "da3584329a263616e277e15462b387addd1b208d",
-)
-
-maven_jar(
- name = "juniversalchardet",
- artifact = "com.github.albfernandez:juniversalchardet:2.0.0",
- sha1 = "28c59f58f5adcc307604602e2aa89e2aca14c554",
-)
-
-SLF4J_VERS = "1.7.26"
-
-maven_jar(
- name = "log-api",
- artifact = "org.slf4j:slf4j-api:" + SLF4J_VERS,
- sha1 = "77100a62c2e6f04b53977b9f541044d7d722693d",
-)
-
-maven_jar(
- name = "log-ext",
- artifact = "org.slf4j:slf4j-ext:" + SLF4J_VERS,
- sha1 = "31cdf122e000322e9efcb38913e9ab07825b17ef",
-)
-
-maven_jar(
- name = "impl-log4j",
- artifact = "org.slf4j:slf4j-log4j12:" + SLF4J_VERS,
- sha1 = "12f5c685b71c3027fd28bcf90528ec4ec74bf818",
-)
-
-maven_jar(
- name = "jcl-over-slf4j",
- artifact = "org.slf4j:jcl-over-slf4j:" + SLF4J_VERS,
- sha1 = "33fbc2d93de829fa5e263c5ce97f5eab8f57d53e",
-)
-
-maven_jar(
- name = "log4j",
- artifact = "log4j:log4j:1.2.17",
- sha1 = "5af35056b4d257e4b64b9e8069c0746e8b08629f",
-)
-
-maven_jar(
- name = "json-smart",
- artifact = "net.minidev:json-smart:1.1.1",
- sha1 = "24a2f903d25e004de30ac602c5b47f2d4e420a59",
-)
-
-maven_jar(
- name = "args4j",
- artifact = "args4j:args4j:2.33",
- sha1 = "bd87a75374a6d6523de82fef51fc3cfe9baf9fc9",
-)
-
-maven_jar(
- name = "commons-codec",
- artifact = "commons-codec:commons-codec:1.10",
- sha1 = "4b95f4897fa13f2cd904aee711aeafc0c5295cd8",
-)
-
-# When upgrading commons-compress, also upgrade tukaani-xz
-maven_jar(
- name = "commons-compress",
- artifact = "org.apache.commons:commons-compress:1.18",
- sha1 = "1191f9f2bc0c47a8cce69193feb1ff0a8bcb37d5",
-)
-
-maven_jar(
- name = "commons-lang",
- artifact = "commons-lang:commons-lang:2.6",
- sha1 = "0ce1edb914c94ebc388f086c6827e8bdeec71ac2",
-)
-
-maven_jar(
- name = "commons-lang3",
- artifact = "org.apache.commons:commons-lang3:3.8.1",
- sha1 = "6505a72a097d9270f7a9e7bf42c4238283247755",
-)
-
-maven_jar(
- name = "commons-text",
- artifact = "org.apache.commons:commons-text:1.2",
- sha1 = "74acdec7237f576c4803fff0c1008ab8a3808b2b",
-)
-
-maven_jar(
- name = "commons-dbcp",
- artifact = "commons-dbcp:commons-dbcp:1.4",
- sha1 = "30be73c965cc990b153a100aaaaafcf239f82d39",
-)
-
-# Transitive dependency of commons-dbcp, do not update without
-# also updating commons-dbcp
-maven_jar(
- name = "commons-pool",
- artifact = "commons-pool:commons-pool:1.5.5",
- sha1 = "7d8ffbdc47aa0c5a8afe5dc2aaf512f369f1d19b",
-)
-
-maven_jar(
- name = "commons-net",
- artifact = "commons-net:commons-net:3.6",
- sha1 = "b71de00508dcb078d2b24b5fa7e538636de9b3da",
-)
-
-maven_jar(
- name = "commons-validator",
- artifact = "commons-validator:commons-validator:1.6",
- sha1 = "e989d1e87cdd60575df0765ed5bac65c905d7908",
-)
-
-maven_jar(
- name = "automaton",
- artifact = "dk.brics:automaton:1.12-1",
- sha1 = "959a0c62f9a5c2309e0ad0b0589c74d69e101241",
-)
-
-COMMONMARK_VERS = "0.10.0"
-
-# commonmark must match the version used in Gitiles
-maven_jar(
- name = "commonmark",
- artifact = "com.atlassian.commonmark:commonmark:" + COMMONMARK_VERS,
- sha1 = "119cb7bedc3570d9ecb64ec69ab7686b5c20559b",
-)
-
-maven_jar(
- name = "cm-autolink",
- artifact = "com.atlassian.commonmark:commonmark-ext-autolink:" + COMMONMARK_VERS,
- sha1 = "a6056a5efbd68f57d420bc51bbc54b28a5d3c56b",
-)
-
-maven_jar(
- name = "gfm-strikethrough",
- artifact = "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:" + COMMONMARK_VERS,
- sha1 = "40837da951b421b545edddac57012e15fcc9e63c",
-)
-
-maven_jar(
- name = "gfm-tables",
- artifact = "com.atlassian.commonmark:commonmark-ext-gfm-tables:" + COMMONMARK_VERS,
- sha1 = "c075db2a3301100cf70c7dced8ecf86b494458a2",
-)
-
-FLEXMARK_VERS = "0.50.42"
-
-maven_jar(
- name = "flexmark",
- artifact = "com.vladsch.flexmark:flexmark:" + FLEXMARK_VERS,
- sha1 = "ed537d7bc31883b008cc17d243a691c7efd12a72",
-)
-
-maven_jar(
- name = "flexmark-ext-abbreviation",
- artifact = "com.vladsch.flexmark:flexmark-ext-abbreviation:" + FLEXMARK_VERS,
- sha1 = "dc27c3e7abbc8d2cfb154f41c68645c365bb9d22",
-)
-
-maven_jar(
- name = "flexmark-ext-anchorlink",
- artifact = "com.vladsch.flexmark:flexmark-ext-anchorlink:" + FLEXMARK_VERS,
- sha1 = "6a8edb0165f695c9c19b7143a7fbd78c25c3b99c",
-)
-
-maven_jar(
- name = "flexmark-ext-autolink",
- artifact = "com.vladsch.flexmark:flexmark-ext-autolink:" + FLEXMARK_VERS,
- sha1 = "5da7a4d009ea08ef2d8714cc73e54a992c6d2d9a",
-)
-
-maven_jar(
- name = "flexmark-ext-definition",
- artifact = "com.vladsch.flexmark:flexmark-ext-definition:" + FLEXMARK_VERS,
- sha1 = "862d17812654624ed81ce8fc89c5ef819ff45f87",
-)
-
-maven_jar(
- name = "flexmark-ext-emoji",
- artifact = "com.vladsch.flexmark:flexmark-ext-emoji:" + FLEXMARK_VERS,
- sha1 = "f0d7db64cb546798742b1ffc6db316a33f6acd76",
-)
-
-maven_jar(
- name = "flexmark-ext-escaped-character",
- artifact = "com.vladsch.flexmark:flexmark-ext-escaped-character:" + FLEXMARK_VERS,
- sha1 = "6fd9ab77619df417df949721cb29c45914b326f8",
-)
-
-maven_jar(
- name = "flexmark-ext-footnotes",
- artifact = "com.vladsch.flexmark:flexmark-ext-footnotes:" + FLEXMARK_VERS,
- sha1 = "e36bd69e43147cc6e19c3f55e4b27c0fc5a3d88c",
-)
-
-maven_jar(
- name = "flexmark-ext-gfm-issues",
- artifact = "com.vladsch.flexmark:flexmark-ext-gfm-issues:" + FLEXMARK_VERS,
- sha1 = "5c825dd4e4fa4f7ccbe30dc92d7e35cdcb8a8c24",
-)
-
-maven_jar(
- name = "flexmark-ext-gfm-strikethrough",
- artifact = "com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:" + FLEXMARK_VERS,
- sha1 = "3256735fd77e7228bf40f7888b4d3dc56787add4",
-)
-
-maven_jar(
- name = "flexmark-ext-gfm-tables",
- artifact = "com.vladsch.flexmark:flexmark-ext-gfm-tables:" + FLEXMARK_VERS,
- sha1 = "62f0efcfb974756940ebe749fd4eb01323babc29",
-)
-
-maven_jar(
- name = "flexmark-ext-gfm-tasklist",
- artifact = "com.vladsch.flexmark:flexmark-ext-gfm-tasklist:" + FLEXMARK_VERS,
- sha1 = "76d4971ad9ce02f0e70351ab6bd06ad8e405e40d",
-)
-
-maven_jar(
- name = "flexmark-ext-gfm-users",
- artifact = "com.vladsch.flexmark:flexmark-ext-gfm-users:" + FLEXMARK_VERS,
- sha1 = "7b0fc7e42e4da508da167fcf8e1cbf9ba7e21147",
-)
-
-maven_jar(
- name = "flexmark-ext-ins",
- artifact = "com.vladsch.flexmark:flexmark-ext-ins:" + FLEXMARK_VERS,
- sha1 = "9e51809867b9c4db0fb1c29599b4574e3d2a78e9",
-)
-
-maven_jar(
- name = "flexmark-ext-jekyll-front-matter",
- artifact = "com.vladsch.flexmark:flexmark-ext-jekyll-front-matter:" + FLEXMARK_VERS,
- sha1 = "44eb6dbb33b3831d3b40af938ddcd99c9c16a654",
-)
-
-maven_jar(
- name = "flexmark-ext-superscript",
- artifact = "com.vladsch.flexmark:flexmark-ext-superscript:" + FLEXMARK_VERS,
- sha1 = "35815b8cb91000344d1fe5df21cacde8553d2994",
-)
-
-maven_jar(
- name = "flexmark-ext-tables",
- artifact = "com.vladsch.flexmark:flexmark-ext-tables:" + FLEXMARK_VERS,
- sha1 = "f6768e98c7210b79d5e8bab76fff27eec6db51e6",
-)
-
-maven_jar(
- name = "flexmark-ext-toc",
- artifact = "com.vladsch.flexmark:flexmark-ext-toc:" + FLEXMARK_VERS,
- sha1 = "1968d038fc6c8156f244f5a7eecb34e7e2f33705",
-)
-
-maven_jar(
- name = "flexmark-ext-typographic",
- artifact = "com.vladsch.flexmark:flexmark-ext-typographic:" + FLEXMARK_VERS,
- sha1 = "6549b9862b61c4434a855a733237103df9162849",
-)
-
-maven_jar(
- name = "flexmark-ext-wikilink",
- artifact = "com.vladsch.flexmark:flexmark-ext-wikilink:" + FLEXMARK_VERS,
- sha1 = "e105b09dd35aab6e6f5c54dfe062ee59bd6f786a",
-)
-
-maven_jar(
- name = "flexmark-ext-yaml-front-matter",
- artifact = "com.vladsch.flexmark:flexmark-ext-yaml-front-matter:" + FLEXMARK_VERS,
- sha1 = "b2d3a1e7f3985841062e8d3203617e29c6c21b52",
-)
-
-maven_jar(
- name = "flexmark-formatter",
- artifact = "com.vladsch.flexmark:flexmark-formatter:" + FLEXMARK_VERS,
- sha1 = "a50c6cb10f6d623fc4354a572c583de1372d217f",
-)
-
-maven_jar(
- name = "flexmark-html-parser",
- artifact = "com.vladsch.flexmark:flexmark-html-parser:" + FLEXMARK_VERS,
- sha1 = "46c075f30017e131c1ada8538f1d8eacf652b044",
-)
-
-maven_jar(
- name = "flexmark-profile-pegdown",
- artifact = "com.vladsch.flexmark:flexmark-profile-pegdown:" + FLEXMARK_VERS,
- sha1 = "d9aafd47629959cbeddd731f327ae090fc92b60f",
-)
-
-maven_jar(
- name = "flexmark-util",
- artifact = "com.vladsch.flexmark:flexmark-util:" + FLEXMARK_VERS,
- sha1 = "417a9821d5d80ddacbfecadc6843ae7b259d5112",
-)
-
-# Transitive dependency of flexmark and gitiles
-maven_jar(
- name = "autolink",
- artifact = "org.nibor.autolink:autolink:0.7.0",
- sha1 = "649f9f13422cf50c926febe6035662ae25dc89b2",
-)
-
-GREENMAIL_VERS = "1.5.5"
-
-maven_jar(
- name = "greenmail",
- artifact = "com.icegreen:greenmail:" + GREENMAIL_VERS,
- sha1 = "9ea96384ad2cb8118c22f493b529eb72c212691c",
-)
-
-MAIL_VERS = "1.6.0"
-
-maven_jar(
- name = "mail",
- artifact = "com.sun.mail:javax.mail:" + MAIL_VERS,
- sha1 = "a055c648842c4954c1f7db7254f45d9ad565e278",
-)
-
-MIME4J_VERS = "0.8.1"
-
-maven_jar(
- name = "mime4j-core",
- artifact = "org.apache.james:apache-mime4j-core:" + MIME4J_VERS,
- sha1 = "c62dfe18a3b827a2c626ade0ffba44562ddf3f61",
-)
-
-maven_jar(
- name = "mime4j-dom",
- artifact = "org.apache.james:apache-mime4j-dom:" + MIME4J_VERS,
- sha1 = "f2d653c617004193f3350330d907f77b60c88c56",
-)
-
-maven_jar(
- name = "jsoup",
- artifact = "org.jsoup:jsoup:1.9.2",
- sha1 = "5e3bda828a80c7a21dfbe2308d1755759c2fd7b4",
-)
-
-OW2_VERS = "9.0"
-
-maven_jar(
- name = "ow2-asm",
- artifact = "org.ow2.asm:asm:" + OW2_VERS,
- sha1 = "af582ff60bc567c42d931500c3fdc20e0141ddf9",
-)
-
-maven_jar(
- name = "ow2-asm-analysis",
- artifact = "org.ow2.asm:asm-analysis:" + OW2_VERS,
- sha1 = "4630afefbb43939c739445dde0af1a5729a0fb4e",
-)
-
-maven_jar(
- name = "ow2-asm-commons",
- artifact = "org.ow2.asm:asm-commons:" + OW2_VERS,
- sha1 = "5a34a3a9ac44f362f35d1b27932380b0031a3334",
-)
-
-maven_jar(
- name = "ow2-asm-tree",
- artifact = "org.ow2.asm:asm-tree:" + OW2_VERS,
- sha1 = "9df939f25c556b0c7efe00701d47e77a49837f24",
-)
-
-maven_jar(
- name = "ow2-asm-util",
- artifact = "org.ow2.asm:asm-util:" + OW2_VERS,
- sha1 = "7c059a94ab5eed3347bf954e27fab58e52968848",
-)
-
-AUTO_VALUE_VERSION = "1.7.4"
-
-maven_jar(
- name = "auto-value",
- artifact = "com.google.auto.value:auto-value:" + AUTO_VALUE_VERSION,
- sha1 = "6b126cb218af768339e4d6e95a9b0ae41f74e73d",
-)
-
-maven_jar(
- name = "auto-value-annotations",
- artifact = "com.google.auto.value:auto-value-annotations:" + AUTO_VALUE_VERSION,
- sha1 = "eff48ed53995db2dadf0456426cc1f8700136f86",
-)
-
-AUTO_VALUE_GSON_VERSION = "1.3.1"
-
-maven_jar(
- name = "auto-value-gson-runtime",
- artifact = "com.ryanharter.auto.value:auto-value-gson-runtime:" + AUTO_VALUE_GSON_VERSION,
- sha1 = "addda2ae6cce9f855788274df5de55dde4de7b71",
-)
-
-maven_jar(
- name = "auto-value-gson-extension",
- artifact = "com.ryanharter.auto.value:auto-value-gson-extension:" + AUTO_VALUE_GSON_VERSION,
- sha1 = "0c4c01a3e10e5b10df2e5f5697efa4bb3f453ac1",
-)
-
-maven_jar(
- name = "auto-value-gson-factory",
- artifact = "com.ryanharter.auto.value:auto-value-gson-factory:" + AUTO_VALUE_GSON_VERSION,
- sha1 = "9ed8d79144ee8d60cc94cc11f847b5ed8ee9f19c",
-)
-
-maven_jar(
- name = "javapoet",
- artifact = "com.squareup:javapoet:1.13.0",
- sha1 = "d6562d385049f35eb50403fa86bb11cce76b866a",
-)
-
-maven_jar(
- name = "autotransient",
- artifact = "io.sweers.autotransient:autotransient:1.0.0",
- sha1 = "38b1c630b8e76560221622289f37be40105abb3d",
-)
-
declare_nongoogle_deps()
-maven_jar(
- name = "mime-util",
- artifact = "eu.medsea.mimeutil:mime-util:2.1.3",
- attach_source = False,
- sha1 = "0c9cfae15c74f62491d4f28def0dff1dabe52a47",
-)
-
-PROLOG_VERS = "1.4.4"
-
-PROLOG_REPO = GERRIT
-
-maven_jar(
- name = "prolog-runtime",
- artifact = "com.googlecode.prolog-cafe:prolog-runtime:" + PROLOG_VERS,
- attach_source = False,
- repository = PROLOG_REPO,
- sha1 = "e9a364f4233481cce63239e8e68a6190c8f58acd",
-)
-
-maven_jar(
- name = "prolog-compiler",
- artifact = "com.googlecode.prolog-cafe:prolog-compiler:" + PROLOG_VERS,
- attach_source = False,
- repository = PROLOG_REPO,
- sha1 = "570295026f6aa7b905e423d107cb2e081eecdc04",
-)
-
-maven_jar(
- name = "prolog-io",
- artifact = "com.googlecode.prolog-cafe:prolog-io:" + PROLOG_VERS,
- attach_source = False,
- repository = PROLOG_REPO,
- sha1 = "1f25c4e27d22bdbc31481ee0c962a2a2853e4428",
-)
-
-maven_jar(
- name = "cafeteria",
- artifact = "com.googlecode.prolog-cafe:prolog-cafeteria:" + PROLOG_VERS,
- attach_source = False,
- repository = PROLOG_REPO,
- sha1 = "0e6c2deeaf5054815a561cbd663566fd59b56c6c",
-)
-
-maven_jar(
- name = "guava-retrying",
- artifact = "com.github.rholder:guava-retrying:2.0.0",
- sha1 = "974bc0a04a11cc4806f7c20a34703bd23c34e7f4",
-)
-
-maven_jar(
- name = "jsr305",
- artifact = "com.google.code.findbugs:jsr305:3.0.1",
- sha1 = "f7be08ec23c21485b9b5a1cf1654c2ec8c58168d",
-)
-
-GITILES_VERS = "0.4-1"
-
-GITILES_REPO = GERRIT
-
-maven_jar(
- name = "blame-cache",
- artifact = "com.google.gitiles:blame-cache:" + GITILES_VERS,
- attach_source = False,
- repository = GITILES_REPO,
- sha1 = "0df80c6b8822147e1f116fd7804b8a0de544f402",
-)
-
-maven_jar(
- name = "gitiles-servlet",
- artifact = "com.google.gitiles:gitiles-servlet:" + GITILES_VERS,
- repository = GITILES_REPO,
- sha1 = "60870897d22b840e65623fd024eabd9cc9706ebe",
-)
-
-# prettify must match the version used in Gitiles
-maven_jar(
- name = "prettify",
- artifact = "com.github.twalcari:java-prettify:1.2.2",
- sha1 = "b8ba1c1eb8b2e45cfd465d01218c6060e887572e",
-)
-
-maven_jar(
- name = "html-types",
- artifact = "com.google.common.html.types:types:1.0.8",
- sha1 = "9e9cf7bc4b2a60efeb5f5581fe46d17c068e0777",
-)
-
-maven_jar(
- name = "icu4j",
- artifact = "com.ibm.icu:icu4j:57.1",
- sha1 = "198ea005f41219f038f4291f0b0e9f3259730e92",
-)
-
-# When updating Bouncy Castle, also update it in bazlets.
-BC_VERS = "1.61"
-
-maven_jar(
- name = "bcprov",
- artifact = "org.bouncycastle:bcprov-jdk15on:" + BC_VERS,
- sha1 = "00df4b474e71be02c1349c3292d98886f888d1f7",
-)
-
-maven_jar(
- name = "bcpg",
- artifact = "org.bouncycastle:bcpg-jdk15on:" + BC_VERS,
- sha1 = "422656435514ab8a28752b117d5d2646660a0ace",
-)
-
-maven_jar(
- name = "bcpkix",
- artifact = "org.bouncycastle:bcpkix-jdk15on:" + BC_VERS,
- sha1 = "89bb3aa5b98b48e584eee2a7401b7682a46779b4",
-)
-
-maven_jar(
- name = "h2",
- artifact = "com.h2database:h2:1.3.176",
- sha1 = "fd369423346b2f1525c413e33f8cf95b09c92cbd",
-)
-
-HTTPCOMP_VERS = "4.5.2"
-
-maven_jar(
- name = "fluent-hc",
- artifact = "org.apache.httpcomponents:fluent-hc:" + HTTPCOMP_VERS,
- sha1 = "7bfdfa49de6d720ad3c8cedb6a5238eec564dfed",
-)
-
-maven_jar(
- name = "httpclient",
- artifact = "org.apache.httpcomponents:httpclient:" + HTTPCOMP_VERS,
- sha1 = "733db77aa8d9b2d68015189df76ab06304406e50",
-)
-
-maven_jar(
- name = "httpcore",
- artifact = "org.apache.httpcomponents:httpcore:4.4.4",
- sha1 = "b31526a230871fbe285fbcbe2813f9c0839ae9b0",
-)
-
-# Test-only dependencies below.
-
-maven_jar(
- name = "junit",
- artifact = "junit:junit:4.12",
- sha1 = "2973d150c0dc1fefe998f834810d68f278ea58ec",
-)
-
-maven_jar(
- name = "hamcrest-core",
- artifact = "org.hamcrest:hamcrest-core:1.3",
- sha1 = "42a25dc3219429f0e5d060061f71acb49bf010a0",
-)
-
-maven_jar(
- name = "diffutils",
- artifact = "com.googlecode.java-diff-utils:diffutils:1.3.0",
- sha1 = "7e060dd5b19431e6d198e91ff670644372f60fbd",
-)
-
-JETTY_VERS = "9.4.36.v20210114"
-
-maven_jar(
- name = "jetty-servlet",
- artifact = "org.eclipse.jetty:jetty-servlet:" + JETTY_VERS,
- sha1 = "b189e52a5ee55ae172e4e99e29c5c314f5daf4b9",
-)
-
-maven_jar(
- name = "jetty-security",
- artifact = "org.eclipse.jetty:jetty-security:" + JETTY_VERS,
- sha1 = "42030d6ed7dfc0f75818cde0adcf738efc477574",
-)
-
-maven_jar(
- name = "jetty-server",
- artifact = "org.eclipse.jetty:jetty-server:" + JETTY_VERS,
- sha1 = "88a7d342974aadca658e7386e8d0fcc5c0788f41",
-)
-
-maven_jar(
- name = "jetty-jmx",
- artifact = "org.eclipse.jetty:jetty-jmx:" + JETTY_VERS,
- sha1 = "bb3847eabe085832aeaedd30e872b40931632e54",
-)
-
-maven_jar(
- name = "jetty-http",
- artifact = "org.eclipse.jetty:jetty-http:" + JETTY_VERS,
- sha1 = "1eee89a55e04ff94df0f85d95200fc48acb43d86",
-)
-
-maven_jar(
- name = "jetty-io",
- artifact = "org.eclipse.jetty:jetty-io:" + JETTY_VERS,
- sha1 = "84a8faf9031eb45a5a2ddb7681e22c483d81ab3a",
-)
-
-maven_jar(
- name = "jetty-util",
- artifact = "org.eclipse.jetty:jetty-util:" + JETTY_VERS,
- sha1 = "925257fbcca6b501a25252c7447dbedb021f7404",
-)
-
-maven_jar(
- name = "jetty-util-ajax",
- artifact = "org.eclipse.jetty:jetty-util-ajax:" + JETTY_VERS,
- sha1 = "2f478130c21787073facb64d7242e06f94980c60",
- src_sha1 = "7153d7ca38878d971fd90992c303bb7719ba7a21",
-)
-
-maven_jar(
- name = "asciidoctor",
- artifact = "org.asciidoctor:asciidoctorj:1.5.7",
- sha1 = "8e8c1d8fc6144405700dd8df3b177f2801ac5987",
-)
-
-maven_jar(
- name = "javax-activation",
- artifact = "javax.activation:activation:1.1.1",
- sha1 = "485de3a253e23f645037828c07f1d7f1af40763a",
-)
-
-maven_jar(
- name = "javax-annotation",
- artifact = "javax.annotation:javax.annotation-api:1.3.2",
- sha1 = "934c04d3cfef185a8008e7bf34331b79730a9d43",
-)
-
-maven_jar(
- name = "mockito",
- artifact = "org.mockito:mockito-core:3.3.3",
- sha1 = "4878395d4e63173f3825e17e5e0690e8054445f1",
-)
-
-BYTE_BUDDY_VERSION = "1.10.7"
-
-maven_jar(
- name = "bytebuddy",
- artifact = "net.bytebuddy:byte-buddy:" + BYTE_BUDDY_VERSION,
- sha1 = "1eefb7dd1b032b33c773ca0a17d5cc9e6b56ea1a",
-)
-
-maven_jar(
- name = "bytebuddy-agent",
- artifact = "net.bytebuddy:byte-buddy-agent:" + BYTE_BUDDY_VERSION,
- sha1 = "c472fad33f617228601172682aa64f8b78508045",
-)
-
-maven_jar(
- name = "objenesis",
- artifact = "org.objenesis:objenesis:3.0.1",
- sha1 = "11cfac598df9dc48bb9ed9357ed04212694b7808",
-)
-
load("@build_bazel_rules_nodejs//:index.bzl", "yarn_install")
yarn_install(
diff --git a/contrib/convertkey/src/main/java/com/googlesource/gerrit/convertkey/ConvertKey.java b/contrib/convertkey/src/main/java/com/googlesource/gerrit/convertkey/ConvertKey.java
deleted file mode 100644
index 08a529c..0000000
--- a/contrib/convertkey/src/main/java/com/googlesource/gerrit/convertkey/ConvertKey.java
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.googlesource.gerrit.convertkey;
-
-import com.jcraft.jsch.HostKey;
-import com.jcraft.jsch.JSchException;
-import java.io.File;
-import java.io.IOException;
-import java.io.StringWriter;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import org.apache.sshd.common.util.Buffer;
-import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
-import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
-
-public class ConvertKey {
- public static void main(String[] args)
- throws GeneralSecurityException, JSchException, IOException {
- SimpleGeneratorHostKeyProvider p;
-
- if (args.length != 1) {
- System.err.println("Error: requires path to the SSH host key");
- return;
- } else {
- File file = new File(args[0]);
- if (!file.exists() || !file.isFile() || !file.canRead()) {
- System.err.println("Error: ssh key should exist and be readable");
- return;
- }
- }
-
- p = new SimpleGeneratorHostKeyProvider();
- // Gerrit's SSH "simple" keys are always RSA.
- p.setPath(args[0]);
- p.setAlgorithm("RSA");
- Iterable<KeyPair> keys = p.loadKeys(); // forces the key to generate.
- for (KeyPair k : keys) {
- System.out.println("Public Key (" + k.getPublic().getAlgorithm() + "):");
- // From Gerrit's SshDaemon class; use JSch to get the public
- // key/type
- final Buffer buf = new Buffer();
- buf.putRawPublicKey(k.getPublic());
- final byte[] keyBin = buf.getCompactData();
- HostKey pub = new HostKey("localhost", keyBin);
- System.out.println(pub.getType() + " " + pub.getKey());
- System.out.println("Private Key:");
- // Use Bouncy Castle to write the private key back in PEM format
- // (PKCS#1)
- // http://stackoverflow.com/questions/25129822/export-rsa-public-key-to-pem-string-using-java
- StringWriter privout = new StringWriter();
- JcaPEMWriter privWriter = new JcaPEMWriter(privout);
- privWriter.writeObject(k.getPrivate());
- privWriter.close();
- System.out.println(privout);
- }
- }
-}
diff --git a/java/Main.java b/java/Main.java
index 11d8234..09c8c76 100644
--- a/java/Main.java
+++ b/java/Main.java
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+@SuppressWarnings("DefaultPackage")
public final class Main {
private static final String FLOGGER_BACKEND_PROPERTY = "flogger.backend_factory";
private static final String FLOGGER_LOGGING_CONTEXT = "flogger.logging_context";
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index c68a4fc..ec6c7f1 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -26,6 +26,7 @@
import static com.google.gerrit.entities.Patch.COMMIT_MSG;
import static com.google.gerrit.entities.Patch.MERGE_LIST;
import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.NON_VISIBLE_CHANGES;
+import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.TOPIC_CLOSURE;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
@@ -38,12 +39,15 @@
import com.github.rholder.retry.BlockStrategy;
import com.google.common.base.Strings;
+import com.google.common.base.Ticker;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.jimfs.Jimfs;
import com.google.common.primitives.Chars;
+import com.google.common.testing.FakeTicker;
import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
import com.google.gerrit.acceptance.PushOneCommit.Result;
import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
@@ -78,6 +82,7 @@
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
+import com.google.gerrit.extensions.api.changes.SubmittedTogetherOption;
import com.google.gerrit.extensions.api.projects.BranchApi;
import com.google.gerrit.extensions.api.projects.BranchInfo;
import com.google.gerrit.extensions.api.projects.BranchInput;
@@ -163,7 +168,6 @@
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.security.KeyPair;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.ArrayList;
@@ -290,6 +294,7 @@
@Inject protected ChangeNotes.Factory notesFactory;
@Inject protected BatchAbandon batchAbandon;
@Inject protected TestSshKeys sshKeys;
+ @Inject protected TestTicker testTicker;
protected EventRecorder eventRecorder;
protected GerritServer server;
@@ -580,8 +585,7 @@
&& SshMode.useSsh()
&& (adminSshSession == null || userSshSession == null)) {
// Create Ssh sessions
- KeyPair adminKeyPair = sshKeys.getKeyPair(admin);
- SshSessionFactory.initSsh(adminKeyPair);
+ SshSessionFactory.initSsh();
Context ctx = newRequestContext(user);
atrScope.set(ctx);
userSshSession = ctx.getSession();
@@ -656,7 +660,10 @@
}
protected Project.NameKey createProjectOverAPI(
- String nameSuffix, Project.NameKey parent, boolean createEmptyCommit, SubmitType submitType)
+ String nameSuffix,
+ @Nullable Project.NameKey parent,
+ boolean createEmptyCommit,
+ @Nullable SubmitType submitType)
throws RestApiException {
ProjectInput in = new ProjectInput();
in.name = name(nameSuffix);
@@ -703,6 +710,8 @@
}
SystemReader.setInstance(oldSystemReader);
oldSystemReader = null;
+ // Set useDefaultTicker in afterTest, so the next beforeTest will use the default ticker
+ testTicker.useDefaultTicker();
}
protected void closeSsh() {
@@ -1195,9 +1204,37 @@
}
protected void assertSubmittedTogether(String chId, String... expected) throws Exception {
- List<ChangeInfo> actual = gApi.changes().id(chId).submittedTogether();
+ assertSubmittedTogether(chId, ImmutableSet.of(), expected);
+ }
+
+ protected void assertSubmittedTogetherWithTopicClosure(String chId, String... expected)
+ throws Exception {
+ assertSubmittedTogether(chId, ImmutableSet.of(TOPIC_CLOSURE), expected);
+ }
+
+ protected void assertSubmittedTogether(
+ String chId,
+ ImmutableSet<SubmittedTogetherOption> submittedTogetherOptions,
+ String... expected)
+ throws Exception {
+ // This does not include NON_VISIBILE_CHANGES
+ List<ChangeInfo> actual =
+ submittedTogetherOptions.isEmpty()
+ ? gApi.changes().id(chId).submittedTogether()
+ : gApi.changes()
+ .id(chId)
+ .submittedTogether(EnumSet.copyOf(submittedTogetherOptions))
+ .changes;
+
+ EnumSet<SubmittedTogetherOption> enumSetIncludingNonVisibleChanges =
+ submittedTogetherOptions.isEmpty()
+ ? EnumSet.of(NON_VISIBLE_CHANGES)
+ : EnumSet.copyOf(submittedTogetherOptions);
+ enumSetIncludingNonVisibleChanges.add(NON_VISIBLE_CHANGES);
+
+ // This includes NON_VISIBLE_CHANGES for comparison.
SubmittedTogetherInfo info =
- gApi.changes().id(chId).submittedTogether(EnumSet.of(NON_VISIBLE_CHANGES));
+ gApi.changes().id(chId).submittedTogether(enumSetIncludingNonVisibleChanges);
assertThat(info.nonVisibleChanges).isEqualTo(0);
assertThat(Iterables.transform(actual, i1 -> i1.changeId))
@@ -1251,10 +1288,10 @@
assertThat(replyTo.getString()).contains(email);
}
- protected Map<BranchNameKey, ObjectId> fetchFromSubmitPreview(String changeId) throws Exception {
- try (BinaryResult result = gApi.changes().id(changeId).current().submitPreview()) {
- return fetchFromBundles(result);
- }
+ protected void assertMailNotReplyTo(Message message, String email) throws Exception {
+ assertThat(message.headers()).containsKey("Reply-To");
+ StringEmailHeader replyTo = (StringEmailHeader) message.headers().get("Reply-To");
+ assertThat(replyTo.getString()).doesNotContain(email);
}
/**
@@ -1609,6 +1646,13 @@
}
}
+ protected void clearSubmitRequirements(Project.NameKey project) throws Exception {
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().clearSubmitRequirements();
+ u.save();
+ }
+ }
+
protected void configLabel(String label, LabelFunction func) throws Exception {
configLabel(label, func, ImmutableList.of());
}
@@ -1764,4 +1808,32 @@
moduleClass.getName());
}
}
+
+ /** {@link Ticker} implementation for mocking without restarting GerritServer */
+ public static class TestTicker extends Ticker {
+ Ticker actualTicker;
+
+ public TestTicker() {
+ useDefaultTicker();
+ }
+
+ /** Switches to system ticker */
+ public Ticker useDefaultTicker() {
+ this.actualTicker = Ticker.systemTicker();
+ return actualTicker;
+ }
+
+ /** Switches to {@link FakeTicker} */
+ public FakeTicker useFakeTicker() {
+ if (!(this.actualTicker instanceof FakeTicker)) {
+ this.actualTicker = new FakeTicker();
+ }
+ return (FakeTicker) actualTicker;
+ }
+
+ @Override
+ public long read() {
+ return actualTicker.read();
+ }
+ }
}
diff --git a/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java b/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
index 50536d8..c4bf20c 100644
--- a/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
+++ b/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
@@ -162,7 +162,7 @@
new Scope() {
@Override
public <T> Provider<T> scope(Key<T> key, Provider<T> creator) {
- return new Provider<T>() {
+ return new Provider<>() {
@Override
public T get() {
return requireContext().get(key, creator);
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index d6dff8f..37b4780 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -39,9 +39,7 @@
"//lib:gson",
"//lib:guava-retrying",
"//lib:jgit",
- "//lib:jgit-ssh-jsch",
"//lib:jgit-ssh-apache",
- "//lib:jsch",
"//lib/commons:compress",
"//lib/commons:lang",
"//lib/flogger:api",
@@ -123,6 +121,7 @@
"//lib/truth",
"//lib/truth:truth-java8-extension",
"//lib/greenmail",
+ "//lib:guava-testlib",
] + TEST_DEPS
java_library(
diff --git a/java/com/google/gerrit/acceptance/ExtensionRegistry.java b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
index 1e5598e..a1dc9e3 100644
--- a/java/com/google/gerrit/acceptance/ExtensionRegistry.java
+++ b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
@@ -14,6 +14,7 @@
package com.google.gerrit.acceptance;
+import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.extensions.api.changes.ActionVisitor;
import com.google.gerrit.extensions.config.CapabilityDefinition;
import com.google.gerrit.extensions.config.DownloadScheme;
@@ -70,6 +71,7 @@
private final DynamicSet<PerformanceLogger> performanceLoggers;
private final DynamicSet<ProjectCreationValidationListener> projectCreationValidationListeners;
private final DynamicSet<SubmitRule> submitRules;
+ private final DynamicSet<SubmitRequirement> submitRequirements;
private final DynamicSet<ChangeMessageModifier> changeMessageModifiers;
private final DynamicSet<ChangeETagComputation> changeETagComputations;
private final DynamicSet<ActionVisitor> actionVisitors;
@@ -108,6 +110,7 @@
DynamicSet<PerformanceLogger> performanceLoggers,
DynamicSet<ProjectCreationValidationListener> projectCreationValidationListeners,
DynamicSet<SubmitRule> submitRules,
+ DynamicSet<SubmitRequirement> submitRequirements,
DynamicSet<ChangeMessageModifier> changeMessageModifiers,
DynamicSet<ChangeETagComputation> changeETagComputations,
DynamicSet<ActionVisitor> actionVisitors,
@@ -142,6 +145,7 @@
this.performanceLoggers = performanceLoggers;
this.projectCreationValidationListeners = projectCreationValidationListeners;
this.submitRules = submitRules;
+ this.submitRequirements = submitRequirements;
this.changeMessageModifiers = changeMessageModifiers;
this.changeETagComputations = changeETagComputations;
this.actionVisitors = actionVisitors;
@@ -216,6 +220,10 @@
return add(submitRules, submitRule);
}
+ public Registration add(SubmitRequirement submitRequirement) {
+ return add(submitRequirements, submitRequirement);
+ }
+
public Registration add(ChangeMessageModifier changeMessageModifier) {
return add(changeMessageModifiers, changeMessageModifier);
}
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index f13f02e..a149f29 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -22,7 +22,9 @@
import com.google.auto.value.AutoValue;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
+import com.google.common.base.Ticker;
import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest.TestTicker;
import com.google.gerrit.acceptance.FakeGroupAuditService.FakeGroupAuditServiceModule;
import com.google.gerrit.acceptance.ReindexGroupsAtStartup.ReindexGroupsAtStartupModule;
import com.google.gerrit.acceptance.ReindexProjectsAtStartup.ReindexProjectsAtStartupModule;
@@ -76,6 +78,7 @@
import com.google.inject.Module;
import com.google.inject.Provides;
import com.google.inject.Singleton;
+import com.google.inject.multibindings.OptionalBinder;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.reflect.Field;
@@ -430,6 +433,23 @@
.to(GitObjectVisibilityChecker.class);
}
});
+ daemon.addAdditionalSysModuleForTesting(
+ new AbstractModule() {
+ @Override
+ protected void configure() {
+ super.configure();
+ // GerritServer isn't restarted between tests. TestTicker allows to replace actual
+ // Ticker in tests without restarting server and transparently for other code.
+ // Alternative option with Provider<Ticker> is less convinient, because it affects how
+ // gerrit code should be written - i.e. Ticker must not be stored in fields and must
+ // always be obtained from the provider.
+ TestTicker testTicker = new TestTicker();
+ OptionalBinder.newOptionalBinder(binder(), Ticker.class)
+ .setBinding()
+ .toInstance(testTicker);
+ bind(TestTicker.class).toInstance(testTicker);
+ }
+ });
if (desc.memory()) {
checkArgument(additionalArgs.length == 0, "cannot pass args to in-memory server");
diff --git a/java/com/google/gerrit/acceptance/GitClientVersion.java b/java/com/google/gerrit/acceptance/GitClientVersion.java
index 4c9a32d..2415781 100644
--- a/java/com/google/gerrit/acceptance/GitClientVersion.java
+++ b/java/com/google/gerrit/acceptance/GitClientVersion.java
@@ -16,6 +16,8 @@
import static java.util.stream.Collectors.joining;
+import com.google.common.base.Splitter;
+import java.util.List;
import java.util.stream.IntStream;
/** Class to parse and represent version of git-core client */
@@ -38,11 +40,11 @@
*/
public GitClientVersion(String version) {
// "git version x.y.z", at Google "git version x.y.z.gXXXXXXXXXX-goog"
- String parts[] = version.split(" ")[2].split("\\.");
- int numParts = Math.min(parts.length, 3); // ignore Google-specific part of the version
+ List<String> parts = Splitter.on(".").splitToList(Splitter.on(" ").splitToList(version).get(2));
+ int numParts = Math.min(parts.size(), 3); // ignore Google-specific part of the version
v = new int[numParts];
for (int i = 0; i < numParts; i++) {
- v[i] = Integer.valueOf(parts[i]);
+ v[i] = Integer.valueOf(parts.get(i));
}
}
diff --git a/java/com/google/gerrit/acceptance/InProcessProtocol.java b/java/com/google/gerrit/acceptance/InProcessProtocol.java
index 83c63f9..15be85c 100644
--- a/java/com/google/gerrit/acceptance/InProcessProtocol.java
+++ b/java/com/google/gerrit/acceptance/InProcessProtocol.java
@@ -100,7 +100,7 @@
new Scope() {
@Override
public <T> Provider<T> scope(Key<T> key, Provider<T> creator) {
- return new Provider<T>() {
+ return new Provider<>() {
@Override
public T get() {
Context ctx = current.get();
diff --git a/java/com/google/gerrit/acceptance/SshSessionJsch.java b/java/com/google/gerrit/acceptance/SshSessionJsch.java
deleted file mode 100644
index a86c2d6..0000000
--- a/java/com/google/gerrit/acceptance/SshSessionJsch.java
+++ /dev/null
@@ -1,174 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance;
-
-import static java.nio.charset.StandardCharsets.US_ASCII;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.gerrit.acceptance.testsuite.account.TestAccount;
-import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
-import com.jcraft.jsch.ChannelExec;
-import com.jcraft.jsch.JSch;
-import com.jcraft.jsch.JSchException;
-import com.jcraft.jsch.Session;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.io.StringWriter;
-import java.net.InetSocketAddress;
-import java.nio.charset.StandardCharsets;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Properties;
-import java.util.Scanner;
-import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
-import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
-import org.bouncycastle.util.io.pem.PemObject;
-import org.eclipse.jgit.transport.JschConfigSessionFactory;
-import org.eclipse.jgit.transport.OpenSshConfig.Host;
-import org.eclipse.jgit.transport.SshSessionFactory;
-import org.eclipse.jgit.util.FS;
-
-public class SshSessionJsch extends SshSession {
-
- private Session session;
-
- public static void initClient(KeyPair keyPair) {
- Properties config = new Properties();
- config.put("StrictHostKeyChecking", "no");
- JSch.setConfig(config);
-
- // register a JschConfigSessionFactory that adds the private key as identity
- // to the JSch instance of JGit so that SSH communication via JGit can
- // succeed
- SshSessionFactory.setInstance(
- new JschConfigSessionFactory() {
- @Override
- protected void configure(Host hc, Session session) {
- try {
- JSch jsch = getJSch(hc, FS.DETECTED);
- jsch.addIdentity(
- "KeyPair", privateKey(keyPair), TestSshKeys.publicKeyBlob(keyPair), null);
- } catch (JSchException | GeneralSecurityException | IOException e) {
- throw new RuntimeException(e);
- }
- }
- });
- }
-
- public static KeyPairGenerator initKeyPairGenerator() throws NoSuchAlgorithmException {
- KeyPairGenerator gen;
- gen = KeyPairGenerator.getInstance("RSA");
- gen.initialize(512, new SecureRandom());
- return gen;
- }
-
- public SshSessionJsch(TestSshKeys sshKeys, InetSocketAddress addr, TestAccount account) {
- super(sshKeys, addr, account);
- }
-
- @Override
- public void open() throws Exception {
- getJschSession();
- }
-
- @Override
- public void close() {
- if (session != null) {
- session.disconnect();
- session = null;
- }
- }
-
- @SuppressWarnings("resource")
- @Override
- public String exec(String command) throws Exception {
- ChannelExec channel = (ChannelExec) getJschSession().openChannel("exec");
- try {
- channel.setCommand(command);
- InputStream in = channel.getInputStream();
- InputStream err = channel.getErrStream();
- channel.connect();
-
- Scanner s = new Scanner(err, UTF_8.name()).useDelimiter("\\A");
- error = s.hasNext() ? s.next() : null;
-
- s = new Scanner(in, UTF_8.name()).useDelimiter("\\A");
- return s.hasNext() ? s.next() : "";
- } finally {
- channel.disconnect();
- }
- }
-
- @SuppressWarnings("resource")
- @Override
- public int execAndReturnStatus(String command) throws Exception {
- ChannelExec channel = (ChannelExec) getJschSession().openChannel("exec");
- try {
- channel.setCommand(command);
- InputStream err = channel.getErrStream();
- channel.connect();
-
- Scanner s = new Scanner(err, UTF_8.name()).useDelimiter("\\A");
- error = s.hasNext() ? s.next() : null;
- return channel.getExitStatus();
- } finally {
- channel.disconnect();
- }
- }
-
- @Override
- public Reader execAndReturnReader(String command) throws Exception {
- ChannelExec channel = (ChannelExec) getJschSession().openChannel("exec");
- channel.setCommand(command);
- channel.connect();
-
- return new InputStreamReader(channel.getInputStream(), StandardCharsets.UTF_8) {
- @Override
- public void close() throws IOException {
- super.close();
- channel.disconnect();
- }
- };
- }
-
- private Session getJschSession() throws Exception {
- if (session == null) {
- KeyPair keyPair = sshKeys.getKeyPair(account);
- JSch jsch = new JSch();
- jsch.addIdentity("KeyPair", privateKey(keyPair), TestSshKeys.publicKeyBlob(keyPair), null);
- String username = getUsername();
- session = jsch.getSession(username, addr.getAddress().getHostAddress(), addr.getPort());
- session.setConfig("StrictHostKeyChecking", "no");
- session.connect();
- }
- return session;
- }
-
- private static byte[] privateKey(KeyPair keyPair) throws IOException {
- // unencrypted form of PKCS#8 file
- JcaPKCS8Generator gen1 = new JcaPKCS8Generator(keyPair.getPrivate(), null);
- PemObject obj1 = gen1.generate();
- StringWriter sw1 = new StringWriter();
- try (JcaPEMWriter pw = new JcaPEMWriter(sw1)) {
- pw.writeObject(obj1);
- }
- return sw1.toString().getBytes(US_ASCII.name());
- }
-}
diff --git a/java/com/google/gerrit/acceptance/SshSessionMina.java b/java/com/google/gerrit/acceptance/SshSessionMina.java
index 3b0ba3b..89096e4 100644
--- a/java/com/google/gerrit/acceptance/SshSessionMina.java
+++ b/java/com/google/gerrit/acceptance/SshSessionMina.java
@@ -16,6 +16,7 @@
import static com.google.common.io.RecursiveDeleteOption.ALLOW_INSECURE;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.nio.file.Files.createTempDirectory;
import com.google.common.io.CharSink;
import com.google.common.io.Files;
@@ -140,7 +141,7 @@
+ addr.getPort());
// TODO(davido): Switch to memory only key resolving mode.
- File userhome = Files.createTempDir();
+ File userhome = createTempDirectory("home-").toFile();
FS fs = FS.DETECTED.setUserHome(userhome);
File sshDir = new File(userhome, ".ssh");
@@ -168,6 +169,7 @@
MoreFiles.deleteRecursively(userhome.toPath(), ALLOW_INSECURE);
} catch (IOException e) {
e.printStackTrace();
+ throw new RuntimeException("Failed to cleanup userhome", e);
}
});
}
diff --git a/java/com/google/gerrit/acceptance/rest/PluginResource.java b/java/com/google/gerrit/acceptance/rest/PluginResource.java
index 745d4fa..56710b9 100644
--- a/java/com/google/gerrit/acceptance/rest/PluginResource.java
+++ b/java/com/google/gerrit/acceptance/rest/PluginResource.java
@@ -20,6 +20,5 @@
public class PluginResource extends ConfigResource {
- static final TypeLiteral<RestView<PluginResource>> PLUGIN_KIND =
- new TypeLiteral<RestView<PluginResource>>() {};
+ static final TypeLiteral<RestView<PluginResource>> PLUGIN_KIND = new TypeLiteral<>() {};
}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
index 3b15b57..580f10f 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
@@ -48,8 +48,8 @@
import com.google.inject.Inject;
import java.io.IOException;
import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Arrays;
-import java.util.Date;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -138,7 +138,7 @@
try (Repository repository = repositoryManager.openRepository(project);
ObjectInserter objectInserter = repository.newObjectInserter();
RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
IdentifiedUser changeOwner = getChangeOwner(changeCreation);
PersonIdent authorAndCommitter =
changeOwner.newCommitterIdent(now, serverIdent.getTimeZone());
@@ -431,7 +431,7 @@
try (Repository repository = repositoryManager.openRepository(project);
ObjectInserter objectInserter = repository.newObjectInserter();
RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
ObjectId newPatchsetCommit =
createPatchsetCommit(
repository, revWalk, objectInserter, changeNotes, patchsetCreation, now);
@@ -457,7 +457,7 @@
ObjectInserter objectInserter,
ChangeNotes changeNotes,
TestPatchsetCreation patchsetCreation,
- Timestamp now)
+ Instant now)
throws IOException, BadRequestException {
ObjectId oldPatchsetCommitId = changeNotes.getCurrentPatchSet().commitId();
RevCommit oldPatchsetCommit = repository.parseCommit(oldPatchsetCommitId);
@@ -494,10 +494,13 @@
return Optional.ofNullable(oldPatchsetCommit.getAuthorIdent()).orElse(serverIdent);
}
- private PersonIdent getCommitter(RevCommit oldPatchsetCommit, Timestamp now) {
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
+ private PersonIdent getCommitter(RevCommit oldPatchsetCommit, Instant now) {
PersonIdent oldPatchsetCommitter =
Optional.ofNullable(oldPatchsetCommit.getCommitterIdent()).orElse(serverIdent);
- if (asSeconds(now) == asSeconds(oldPatchsetCommitter.getWhen())) {
+ if (asSeconds(now) == asSeconds(oldPatchsetCommitter.getWhen().toInstant())) {
/* We need to ensure that the resulting commit SHA-1 is different from the old patchset.
* In real situations, this automatically happens as two patchsets won't have exactly the
* same commit timestamp even when the tree and commit message are the same. In tests,
@@ -505,13 +508,13 @@
* We could of course require that tests must use TestTimeUtil#setClockStep but
* that would be an unnecessary nuisance for test writers. Hence, go with a simple solution
* here and simply add a second. */
- now = Timestamp.from(now.toInstant().plusSeconds(1));
+ now = now.plusSeconds(1);
}
- return new PersonIdent(oldPatchsetCommitter, now);
+ return new PersonIdent(oldPatchsetCommitter, Timestamp.from(now));
}
- private long asSeconds(Date date) {
- return date.getTime() / 1000;
+ private long asSeconds(Instant date) {
+ return date.getEpochSecond();
}
private ImmutableList<ObjectId> getParents(
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
index eda6c7e..9b393ef 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
@@ -38,7 +38,7 @@
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -106,7 +106,7 @@
try (Repository repository = repositoryManager.openRepository(project);
ObjectInserter objectInserter = repository.newObjectInserter();
RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
IdentifiedUser author = getAuthor(commentCreation);
CommentAdditionOp commentAdditionOp = new CommentAdditionOp(commentCreation);
@@ -165,8 +165,7 @@
short side = commentCreation.side().orElse(CommentSide.PATCHSET_COMMIT).getNumericSide();
Boolean unresolved = commentCreation.unresolved().orElse(null);
String parentUuid = commentCreation.parentUuid().orElse(null);
- Timestamp createdOn =
- commentCreation.createdOn().map(Timestamp::from).orElse(context.getWhen());
+ Instant createdOn = commentCreation.createdOn().orElse(context.getWhen());
HumanComment newComment =
commentsUtil.newHumanComment(
context.getNotes(),
@@ -202,7 +201,7 @@
try (Repository repository = repositoryManager.openRepository(project);
ObjectInserter objectInserter = repository.newObjectInserter();
RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
IdentifiedUser author = getAuthor(robotCommentCreation);
RobotCommentAdditionOp robotCommentAdditionOp =
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/TestGroup.java b/java/com/google/gerrit/acceptance/testsuite/group/TestGroup.java
index c885353..0b21e2c 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/TestGroup.java
+++ b/java/com/google/gerrit/acceptance/testsuite/group/TestGroup.java
@@ -18,7 +18,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Optional;
@AutoValue
@@ -40,7 +40,7 @@
public abstract boolean visibleToAll();
- public abstract Timestamp createdOn();
+ public abstract Instant createdOn();
public abstract ImmutableSet<Account.Id> members();
@@ -67,7 +67,7 @@
public abstract Builder visibleToAll(boolean visibleToAll);
- public abstract Builder createdOn(Timestamp createdOn);
+ public abstract Builder createdOn(Instant createdOn);
public abstract Builder members(ImmutableSet<Account.Id> members);
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/SshSessionFactory.java b/java/com/google/gerrit/acceptance/testsuite/request/SshSessionFactory.java
index d5dd28a..3442b6e 100644
--- a/java/com/google/gerrit/acceptance/testsuite/request/SshSessionFactory.java
+++ b/java/com/google/gerrit/acceptance/testsuite/request/SshSessionFactory.java
@@ -14,10 +14,7 @@
package com.google.gerrit.acceptance.testsuite.request;
-import static com.google.gerrit.server.config.SshClientImplementation.getFromEnvironment;
-
import com.google.gerrit.acceptance.SshSession;
-import com.google.gerrit.acceptance.SshSessionJsch;
import com.google.gerrit.acceptance.SshSessionMina;
import com.google.gerrit.acceptance.testsuite.account.TestAccount;
import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
@@ -28,25 +25,16 @@
public class SshSessionFactory {
public static SshSession createSession(
TestSshKeys testSshKeys, InetSocketAddress sshAddress, TestAccount testAccount) {
- return getFromEnvironment().isMina()
- ? new SshSessionMina(testSshKeys, sshAddress, testAccount)
- : new SshSessionJsch(testSshKeys, sshAddress, testAccount);
+ return new SshSessionMina(testSshKeys, sshAddress, testAccount);
}
- public static void initSsh(KeyPair keyPair) {
- if (getFromEnvironment().isMina()) {
- SshSessionMina.initClient();
- } else {
- SshSessionJsch.initClient(keyPair);
- }
+ public static void initSsh() {
+ SshSessionMina.initClient();
}
private SshSessionFactory() {}
public static KeyPair genSshKey() throws GeneralSecurityException {
- return (getFromEnvironment().isMina()
- ? SshSessionMina.initKeyPairGenerator()
- : SshSessionJsch.initKeyPairGenerator())
- .generateKeyPair();
+ return SshSessionMina.initKeyPairGenerator().generateKeyPair();
}
}
diff --git a/java/com/google/gerrit/auth/ldap/LdapGroupBackend.java b/java/com/google/gerrit/auth/ldap/LdapGroupBackend.java
index 2947efd..c3870f4 100644
--- a/java/com/google/gerrit/auth/ldap/LdapGroupBackend.java
+++ b/java/com/google/gerrit/auth/ldap/LdapGroupBackend.java
@@ -125,7 +125,7 @@
String groupDn = uuid.get().substring(LDAP_UUID.length());
CurrentUser user = userProvider.get();
- if (!(user.isIdentifiedUser()) || !membershipsOf(user.asIdentifiedUser()).contains(uuid)) {
+ if (!user.isIdentifiedUser() || !membershipsOf(user.asIdentifiedUser()).contains(uuid)) {
try {
if (!existsCache.get(groupDn)) {
return null;
diff --git a/java/com/google/gerrit/auth/ldap/LdapRealm.java b/java/com/google/gerrit/auth/ldap/LdapRealm.java
index 9a9f309..7699799 100644
--- a/java/com/google/gerrit/auth/ldap/LdapRealm.java
+++ b/java/com/google/gerrit/auth/ldap/LdapRealm.java
@@ -199,7 +199,7 @@
String configOption, String suppliedValue, boolean disabledByBackend) {
if (disabledByBackend && !Strings.isNullOrEmpty(suppliedValue)) {
String msg = String.format("LDAP backend doesn't support: ldap.%s", configOption);
- logger.atSevere().log(msg);
+ logger.atSevere().log("%s", msg);
throw new IllegalArgumentException(msg);
}
}
diff --git a/java/com/google/gerrit/common/IoUtil.java b/java/com/google/gerrit/common/IoUtil.java
index 37f6c2c..09a8993 100644
--- a/java/com/google/gerrit/common/IoUtil.java
+++ b/java/com/google/gerrit/common/IoUtil.java
@@ -32,16 +32,27 @@
public final class IoUtil {
public static void copyWithThread(InputStream src, OutputStream dst) {
new Thread("IoUtil-Copy") {
+ // We cannot propagate the exception since this code is running in a background thread.
+ // Printing the stacktrace is the best we can do. Hence ignoring the exception after printing
+ // the stacktrace is OK and it's fine to suppress the warning for the CatchAndPrintStackTrace
+ // bug pattern here.
+ @SuppressWarnings("CatchAndPrintStackTrace")
@Override
public void run() {
try {
+ copyIo();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void copyIo() throws IOException {
+ try {
final byte[] buf = new byte[256];
int n;
while (0 < (n = src.read(buf))) {
dst.write(buf, 0, n);
}
- } catch (IOException e) {
- e.printStackTrace();
} finally {
try {
src.close();
diff --git a/java/com/google/gerrit/common/Version.java b/java/com/google/gerrit/common/Version.java
index 6197be5..bfca4d0 100644
--- a/java/com/google/gerrit/common/Version.java
+++ b/java/com/google/gerrit/common/Version.java
@@ -54,7 +54,7 @@
return vs;
}
} catch (IOException e) {
- logger.atSevere().withCause(e).log(e.getMessage());
+ logger.atSevere().withCause(e).log("%s", e.getMessage());
return "(unknown version)";
}
}
diff --git a/java/com/google/gerrit/entities/Account.java b/java/com/google/gerrit/entities/Account.java
index cd3b27a..303e79f 100644
--- a/java/com/google/gerrit/entities/Account.java
+++ b/java/com/google/gerrit/entities/Account.java
@@ -22,7 +22,7 @@
import com.google.common.primitives.Ints;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Optional;
/**
@@ -123,7 +123,7 @@
public abstract Id id();
/** Date and time the user registered with the review server. */
- public abstract Timestamp registeredOn();
+ public abstract Instant registeredOn();
/** Full name of the user ("Given-name Surname" style). */
@Nullable
@@ -157,7 +157,7 @@
* @param newId unique id, see {@link com.google.gerrit.server.notedb.Sequences#nextAccountId()}.
* @param registeredOn when the account was registered.
*/
- public static Account.Builder builder(Account.Id newId, Timestamp registeredOn) {
+ public static Account.Builder builder(Account.Id newId, Instant registeredOn) {
return new AutoValue_Account.Builder()
.setInactive(false)
.setId(newId)
@@ -196,9 +196,9 @@
* <p>Example output:
*
* <ul>
- * <li>{@code A U. Thor <author@example.com>}: full populated
+ * <li>{@code A U. Thor <author@example.com>}: full populated
* <li>{@code A U. Thor (12)}: missing email address
- * <li>{@code Anonymous Coward <author@example.com>}: missing name
+ * <li>{@code Anonymous Coward <author@example.com>}: missing name
* <li>{@code Anonymous Coward (12)}: missing name and email address
* </ul>
*/
@@ -230,9 +230,9 @@
abstract Builder setId(Id id);
- public abstract Timestamp registeredOn();
+ public abstract Instant registeredOn();
- abstract Builder setRegisteredOn(Timestamp registeredOn);
+ abstract Builder setRegisteredOn(Instant registeredOn);
@Nullable
public abstract String fullName();
diff --git a/java/com/google/gerrit/entities/AccountGroupByIdAudit.java b/java/com/google/gerrit/entities/AccountGroupByIdAudit.java
index 17ddf51..0ef51e5 100644
--- a/java/com/google/gerrit/entities/AccountGroupByIdAudit.java
+++ b/java/com/google/gerrit/entities/AccountGroupByIdAudit.java
@@ -15,7 +15,7 @@
package com.google.gerrit.entities;
import com.google.auto.value.AutoValue;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Optional;
/** Inclusion of an {@link AccountGroup} in another {@link AccountGroup}. */
@@ -33,13 +33,13 @@
public abstract Builder addedBy(Account.Id addedBy);
- public abstract Builder addedOn(Timestamp addedOn);
+ public abstract Builder addedOn(Instant addedOn);
abstract Builder removedBy(Account.Id removedBy);
- abstract Builder removedOn(Timestamp removedOn);
+ abstract Builder removedOn(Instant removedOn);
- public Builder removed(Account.Id removedBy, Timestamp removedOn) {
+ public Builder removed(Account.Id removedBy, Instant removedOn) {
return removedBy(removedBy).removedOn(removedOn);
}
@@ -52,11 +52,11 @@
public abstract Account.Id addedBy();
- public abstract Timestamp addedOn();
+ public abstract Instant addedOn();
public abstract Optional<Account.Id> removedBy();
- public abstract Optional<Timestamp> removedOn();
+ public abstract Optional<Instant> removedOn();
public abstract Builder toBuilder();
diff --git a/java/com/google/gerrit/entities/AccountGroupMemberAudit.java b/java/com/google/gerrit/entities/AccountGroupMemberAudit.java
index 4d191b8..913956e 100644
--- a/java/com/google/gerrit/entities/AccountGroupMemberAudit.java
+++ b/java/com/google/gerrit/entities/AccountGroupMemberAudit.java
@@ -15,7 +15,7 @@
package com.google.gerrit.entities;
import com.google.auto.value.AutoValue;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Optional;
/** Membership of an {@link Account} in an {@link AccountGroup}. */
@@ -35,15 +35,15 @@
abstract Account.Id addedBy();
- public abstract Builder addedOn(Timestamp addedOn);
+ public abstract Builder addedOn(Instant addedOn);
- abstract Timestamp addedOn();
+ abstract Instant addedOn();
abstract Builder removedBy(Account.Id removedBy);
- abstract Builder removedOn(Timestamp removedOn);
+ abstract Builder removedOn(Instant removedOn);
- public Builder removed(Account.Id removedBy, Timestamp removedOn) {
+ public Builder removed(Account.Id removedBy, Instant removedOn) {
return removedBy(removedBy).removedOn(removedOn);
}
@@ -60,11 +60,11 @@
public abstract Account.Id addedBy();
- public abstract Timestamp addedOn();
+ public abstract Instant addedOn();
public abstract Optional<Account.Id> removedBy();
- public abstract Optional<Timestamp> removedOn();
+ public abstract Optional<Instant> removedOn();
public abstract Builder toBuilder();
diff --git a/java/com/google/gerrit/entities/CachedProjectConfig.java b/java/com/google/gerrit/entities/CachedProjectConfig.java
index 8740235..cd65efc 100644
--- a/java/com/google/gerrit/entities/CachedProjectConfig.java
+++ b/java/com/google/gerrit/entities/CachedProjectConfig.java
@@ -226,7 +226,7 @@
try {
parsedProjectLevelConfigsBuilder().put(configFileName, ImmutableConfig.parse(config));
} catch (ConfigInvalidException e) {
- logger.atInfo().withCause(e).log("Config for " + configFileName + " not parsable");
+ logger.atInfo().withCause(e).log("Config for %s not parsable", configFileName);
}
return this;
}
diff --git a/java/com/google/gerrit/entities/Change.java b/java/com/google/gerrit/entities/Change.java
index d1826bc..66e1a96 100644
--- a/java/com/google/gerrit/entities/Change.java
+++ b/java/com/google/gerrit/entities/Change.java
@@ -14,7 +14,6 @@
package com.google.gerrit.entities;
-import static com.google.common.base.Preconditions.checkArgument;
import static com.google.gerrit.entities.RefNames.REFS_CHANGES;
import com.google.auto.value.AutoValue;
@@ -24,7 +23,7 @@
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.annotations.SerializedName;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Arrays;
import java.util.Optional;
@@ -109,18 +108,6 @@
/**
* Parse a Change.Id out of a string representation.
*
- * @deprecated use {@link #tryParse(String)} instead.
- */
- @Deprecated
- public static Id parse(String str) {
- Integer id = Ints.tryParse(str);
- checkArgument(id != null, "invalid change ID: %s", str);
- return Change.id(id);
- }
-
- /**
- * Parse a Change.Id out of a string representation.
- *
* @param str the string to parse
* @return Optional containing the Change.Id, or {@code Optional.empty()} if str does not
* represent a valid Change.Id.
@@ -438,37 +425,37 @@
}
/** Locally assigned unique identifier of the change */
- protected Id changeId;
+ private Id changeId;
/** Globally assigned unique identifier of the change */
- protected Key changeKey;
+ private Key changeKey;
/** When this change was first introduced into the database. */
- protected Timestamp createdOn;
+ private Instant createdOn;
/**
* When was a meaningful modification last made to this record's data
*
* <p>Note, this update timestamp includes its children.
*/
- protected Timestamp lastUpdatedOn;
+ private Instant lastUpdatedOn;
- protected Account.Id owner;
+ private Account.Id owner;
/** The branch (and project) this change merges into. */
- protected BranchNameKey dest;
+ private BranchNameKey dest;
/** Current state code; see {@link Status}. */
- protected char status;
+ private char status;
/** The current patch set. */
- protected int currentPatchSetId;
+ private int currentPatchSetId;
/** Subject from the current patch set. */
- protected String subject;
+ private String subject;
/** Topic name assigned by the user, if any. */
- @Nullable protected String topic;
+ @Nullable private String topic;
/**
* First line of first patch set's commit message.
@@ -476,40 +463,36 @@
* <p>Unlike {@link #subject}, this string does not change if future patch sets change the first
* line.
*/
- @Nullable protected String originalSubject;
+ @Nullable private String originalSubject;
/**
* Unique id for the changes submitted together assigned during merging. Only set if the status is
* MERGED.
*/
- @Nullable protected String submissionId;
+ @Nullable private String submissionId;
/** Allows assigning a change to a user. */
- @Nullable protected Account.Id assignee;
+ @Nullable private Account.Id assignee;
/** Whether the change is private. */
- protected boolean isPrivate;
+ private boolean isPrivate;
/** Whether the change is work in progress. */
- protected boolean workInProgress;
+ private boolean workInProgress;
/** Whether the change has started review. */
- protected boolean reviewStarted;
+ private boolean reviewStarted;
/** References a change that this change reverts. */
- @Nullable protected Id revertOf;
+ @Nullable private Id revertOf;
/** References the source change and patchset that this change was cherry-picked from. */
- @Nullable protected PatchSet.Id cherryPickOf;
+ @Nullable private PatchSet.Id cherryPickOf;
- protected Change() {}
+ Change() {}
public Change(
- Change.Key newKey,
- Change.Id newId,
- Account.Id ownedBy,
- BranchNameKey forBranch,
- Timestamp ts) {
+ Change.Key newKey, Change.Id newId, Account.Id ownedBy, BranchNameKey forBranch, Instant ts) {
changeKey = newKey;
changeId = newId;
createdOn = ts;
@@ -567,19 +550,19 @@
assignee = a;
}
- public Timestamp getCreatedOn() {
+ public Instant getCreatedOn() {
return createdOn;
}
- public void setCreatedOn(Timestamp ts) {
+ public void setCreatedOn(Instant ts) {
createdOn = ts;
}
- public Timestamp getLastUpdatedOn() {
+ public Instant getLastUpdatedOn() {
return lastUpdatedOn;
}
- public void setLastUpdatedOn(Timestamp now) {
+ public void setLastUpdatedOn(Instant now) {
lastUpdatedOn = now;
}
diff --git a/java/com/google/gerrit/entities/ChangeMessage.java b/java/com/google/gerrit/entities/ChangeMessage.java
index cb56c31..609b54c 100644
--- a/java/com/google/gerrit/entities/ChangeMessage.java
+++ b/java/com/google/gerrit/entities/ChangeMessage.java
@@ -16,7 +16,7 @@
import com.google.auto.value.AutoValue;
import com.google.gerrit.common.Nullable;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Objects;
/**
@@ -40,40 +40,40 @@
public abstract String uuid();
}
- protected Key key;
+ private Key key;
/** Who wrote this comment; null if it was written by the Gerrit system. */
- @Nullable protected Account.Id author;
+ @Nullable private Account.Id author;
/** When this comment was drafted. */
- protected Timestamp writtenOn;
+ private Instant writtenOn;
/**
* The text left by the user or Gerrit system in template form, that is free of Gerrit User
* Identifiable Information and can be persisted in data storage.
*/
- @Nullable protected String message;
+ @Nullable private String message;
/** Which patchset (if any) was this message generated from? */
- @Nullable protected PatchSet.Id patchset;
+ @Nullable private PatchSet.Id patchset;
/** Tag associated with change message */
- @Nullable protected String tag;
+ @Nullable private String tag;
/** Real user that added this message on behalf of the user recorded in {@link #author}. */
- @Nullable protected Account.Id realAuthor;
+ @Nullable private Account.Id realAuthor;
- protected ChangeMessage() {}
+ private ChangeMessage() {}
public static ChangeMessage create(
- final ChangeMessage.Key k, @Nullable Account.Id a, Timestamp wo, @Nullable PatchSet.Id psid) {
+ final ChangeMessage.Key k, @Nullable Account.Id a, Instant wo, @Nullable PatchSet.Id psid) {
return create(k, a, wo, psid, /*messageTemplate=*/ null, /*realAuthor=*/ null, /*tag=*/ null);
}
public static ChangeMessage create(
final ChangeMessage.Key k,
@Nullable Account.Id a,
- Timestamp wo,
+ Instant wo,
@Nullable PatchSet.Id psid,
@Nullable String messageTemplate,
@Nullable Account.Id realAuthor,
@@ -103,7 +103,7 @@
return realAuthor != null ? realAuthor : getAuthor();
}
- public Timestamp getWrittenOn() {
+ public Instant getWrittenOn() {
return writtenOn;
}
diff --git a/java/com/google/gerrit/entities/Comment.java b/java/com/google/gerrit/entities/Comment.java
index 37b8620..65a1559 100644
--- a/java/com/google/gerrit/entities/Comment.java
+++ b/java/com/google/gerrit/entities/Comment.java
@@ -18,6 +18,7 @@
import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.gerrit.common.Nullable;
import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Comparator;
import java.util.Objects;
import org.eclipse.jgit.lib.AnyObjectId;
@@ -88,7 +89,7 @@
Key k = (Key) o;
return Objects.equals(uuid, k.uuid)
&& Objects.equals(filename, k.filename)
- && Objects.equals(patchSetId, k.patchSetId);
+ && patchSetId == k.patchSetId;
}
return false;
}
@@ -113,7 +114,7 @@
@Override
public boolean equals(Object o) {
if (o instanceof Identity) {
- return Objects.equals(id, ((Identity) o).id);
+ return id == ((Identity) o).id;
}
return false;
}
@@ -180,10 +181,10 @@
public boolean equals(Object o) {
if (o instanceof Range) {
Range r = (Range) o;
- return Objects.equals(startLine, r.startLine)
- && Objects.equals(startChar, r.startChar)
- && Objects.equals(endLine, r.endLine)
- && Objects.equals(endChar, r.endChar);
+ return startLine == r.startLine
+ && startChar == r.startChar
+ && endLine == r.endLine
+ && endChar == r.endChar;
}
return false;
}
@@ -215,7 +216,10 @@
public Identity author;
protected Identity realAuthor;
+
+ // TODO(issue-15525): Migrate this field from Timestamp to Instant
public Timestamp writtenOn;
+
public short side;
public String message;
public String parentUuid;
@@ -233,13 +237,7 @@
public String serverId;
public Comment(Comment c) {
- this(
- new Key(c.key),
- c.author.getId(),
- new Timestamp(c.writtenOn.getTime()),
- c.side,
- c.message,
- c.serverId);
+ this(new Key(c.key), c.author.getId(), c.writtenOn.toInstant(), c.side, c.message, c.serverId);
this.lineNbr = c.lineNbr;
this.realAuthor = c.realAuthor;
this.parentUuid = c.parentUuid;
@@ -249,21 +247,20 @@
}
public Comment(
- Key key,
- Account.Id author,
- Timestamp writtenOn,
- short side,
- String message,
- String serverId) {
+ Key key, Account.Id author, Instant writtenOn, short side, String message, String serverId) {
this.key = key;
this.author = new Comment.Identity(author);
this.realAuthor = this.author;
- this.writtenOn = writtenOn;
+ this.writtenOn = Timestamp.from(writtenOn);
this.side = side;
this.message = message;
this.serverId = serverId;
}
+ public void setWrittenOn(Instant writtenOn) {
+ this.writtenOn = Timestamp.from(writtenOn);
+ }
+
public void setLineNbrAndRange(
Integer lineNbr, com.google.gerrit.extensions.client.Comment.Range range) {
this.lineNbr = lineNbr != null ? lineNbr : range != null ? range.endLine : 0;
diff --git a/java/com/google/gerrit/entities/EmailHeader.java b/java/com/google/gerrit/entities/EmailHeader.java
index bf5a644..e43b6a3 100644
--- a/java/com/google/gerrit/entities/EmailHeader.java
+++ b/java/com/google/gerrit/entities/EmailHeader.java
@@ -19,7 +19,9 @@
import com.google.common.base.MoreObjects;
import java.io.IOException;
import java.io.Writer;
-import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -127,13 +129,13 @@
}
public static class Date extends EmailHeader {
- private final java.util.Date value;
+ private final Instant value;
- public Date(java.util.Date v) {
+ public Date(Instant v) {
value = v;
}
- public java.util.Date getDate() {
+ public Instant getDate() {
return value;
}
@@ -144,10 +146,12 @@
@Override
public void write(Writer w) throws IOException {
- final SimpleDateFormat fmt;
- // Mon, 1 Jun 2009 10:49:44 -0700
- fmt = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.US);
- w.write(fmt.format(value));
+ // Mon, 1 Jun 2009 10:49:44 +0000
+ w.write(
+ DateTimeFormatter.ofPattern("EEE, d MMM yyyy HH:mm:ss Z")
+ .withLocale(Locale.US)
+ .withZone(ZoneId.of("UTC"))
+ .format(value));
}
@Override
diff --git a/java/com/google/gerrit/entities/GroupDescription.java b/java/com/google/gerrit/entities/GroupDescription.java
index 7054bed..666e8f6 100644
--- a/java/com/google/gerrit/entities/GroupDescription.java
+++ b/java/com/google/gerrit/entities/GroupDescription.java
@@ -15,7 +15,7 @@
package com.google.gerrit.entities;
import com.google.gerrit.common.Nullable;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Set;
/** Group methods exposed by the GroupBackend. */
@@ -55,7 +55,7 @@
boolean isVisibleToAll();
- Timestamp getCreatedOn();
+ Instant getCreatedOn();
Set<Account.Id> getMembers();
diff --git a/java/com/google/gerrit/entities/HumanComment.java b/java/com/google/gerrit/entities/HumanComment.java
index 50bee8d..d287fa0 100644
--- a/java/com/google/gerrit/entities/HumanComment.java
+++ b/java/com/google/gerrit/entities/HumanComment.java
@@ -14,7 +14,7 @@
package com.google.gerrit.entities;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Objects;
/**
@@ -33,7 +33,7 @@
public HumanComment(
Key key,
Account.Id author,
- Timestamp writtenOn,
+ Instant writtenOn,
short side,
String message,
String serverId,
diff --git a/java/com/google/gerrit/entities/InternalGroup.java b/java/com/google/gerrit/entities/InternalGroup.java
index ebfa36a..43c3af3 100644
--- a/java/com/google/gerrit/entities/InternalGroup.java
+++ b/java/com/google/gerrit/entities/InternalGroup.java
@@ -18,7 +18,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.Nullable;
import java.io.Serializable;
-import java.sql.Timestamp;
+import java.time.Instant;
import org.eclipse.jgit.lib.ObjectId;
@AutoValue
@@ -42,7 +42,7 @@
public abstract AccountGroup.UUID getGroupUUID();
- public abstract Timestamp getCreatedOn();
+ public abstract Instant getCreatedOn();
public abstract ImmutableSet<Account.Id> getMembers();
@@ -71,7 +71,7 @@
public abstract Builder setGroupUUID(AccountGroup.UUID groupUuid);
- public abstract Builder setCreatedOn(Timestamp createdOn);
+ public abstract Builder setCreatedOn(Instant createdOn);
public abstract Builder setMembers(ImmutableSet<Account.Id> members);
diff --git a/java/com/google/gerrit/entities/LabelType.java b/java/com/google/gerrit/entities/LabelType.java
index d254752..3541aac 100644
--- a/java/com/google/gerrit/entities/LabelType.java
+++ b/java/com/google/gerrit/entities/LabelType.java
@@ -95,6 +95,8 @@
public abstract String getName();
+ public abstract Optional<String> getDescription();
+
public abstract LabelFunction getFunction();
public abstract boolean isCopyAnyScore();
@@ -141,8 +143,9 @@
}
public static LabelType.Builder builder(String name, List<LabelValue> valueList) {
- return (new AutoValue_LabelType.Builder())
+ return new AutoValue_LabelType.Builder()
.setName(name)
+ .setDescription(Optional.empty())
.setValues(valueList)
.setDefaultValue((short) 0)
.setFunction(LabelFunction.MAX_WITH_BLOCK)
@@ -226,6 +229,8 @@
public abstract static class Builder {
public abstract Builder setName(String name);
+ public abstract Builder setDescription(Optional<String> description);
+
public abstract Builder setFunction(LabelFunction function);
public abstract Builder setCanOverride(boolean canOverride);
diff --git a/java/com/google/gerrit/entities/LabelTypes.java b/java/com/google/gerrit/entities/LabelTypes.java
index 55a9976..a2f2e0b 100644
--- a/java/com/google/gerrit/entities/LabelTypes.java
+++ b/java/com/google/gerrit/entities/LabelTypes.java
@@ -69,7 +69,7 @@
public Comparator<String> nameComparator() {
final Map<String, Integer> positions = positions();
- return new Comparator<String>() {
+ return new Comparator<>() {
@Override
public int compare(String left, String right) {
int lp = position(left);
diff --git a/java/com/google/gerrit/entities/PatchSet.java b/java/com/google/gerrit/entities/PatchSet.java
index b26e5c3..6c52368 100644
--- a/java/com/google/gerrit/entities/PatchSet.java
+++ b/java/com/google/gerrit/entities/PatchSet.java
@@ -22,7 +22,8 @@
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
-import java.sql.Timestamp;
+import com.google.errorprone.annotations.InlineMe;
+import java.time.Instant;
import java.util.List;
import java.util.Optional;
import org.eclipse.jgit.lib.ObjectId;
@@ -41,6 +42,9 @@
* @deprecated use isChangeRef instead.
*/
@Deprecated
+ @InlineMe(
+ replacement = "PatchSet.isChangeRef(name)",
+ imports = "com.google.gerrit.entities.PatchSet")
public static boolean isRef(String name) {
return isChangeRef(name);
}
@@ -159,7 +163,7 @@
public abstract Builder uploader(Account.Id uploader);
- public abstract Builder createdOn(Timestamp createdOn);
+ public abstract Builder createdOn(Instant createdOn);
public abstract Builder groups(Iterable<String> groups);
@@ -206,7 +210,7 @@
* Gerrit, and the old data erroneously did not include a {@code createdOn}, then this method will
* return a timestamp of 0.
*/
- public abstract Timestamp createdOn();
+ public abstract Instant createdOn();
/**
* Opaque group identifier, usually assigned during creation.
diff --git a/java/com/google/gerrit/entities/PatchSetApproval.java b/java/com/google/gerrit/entities/PatchSetApproval.java
index f853f77..608cf0d 100644
--- a/java/com/google/gerrit/entities/PatchSetApproval.java
+++ b/java/com/google/gerrit/entities/PatchSetApproval.java
@@ -16,8 +16,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.primitives.Shorts;
-import java.sql.Timestamp;
-import java.util.Date;
+import java.time.Instant;
import java.util.Optional;
/** An approval (or negative approval) on a patch set. */
@@ -40,6 +39,36 @@
}
}
+ /**
+ * Globally unique identifier.
+ *
+ * <p>The identifier is unique to each granted approval, i.e. approvals, re-added within same
+ * {@link Change} or even {@link PatchSet} have different {@link UUID}.
+ */
+ @AutoValue
+ public abstract static class UUID implements Comparable<UUID> {
+
+ abstract String uuid();
+
+ public String get() {
+ return uuid();
+ }
+
+ @Override
+ public final int compareTo(UUID o) {
+ return uuid().compareTo(o.uuid());
+ }
+
+ @Override
+ public final String toString() {
+ return get();
+ }
+ }
+
+ public static UUID uuid(String n) {
+ return new AutoValue_PatchSetApproval_UUID(n);
+ }
+
public static Builder builder() {
return new AutoValue_PatchSetApproval.Builder().postSubmit(false).copied(false);
}
@@ -50,17 +79,24 @@
public abstract Key key();
+ /**
+ * {@link UUID} of {@link PatchSetApproval}.
+ *
+ * <p>Optional, since it might be missing for approvals, granted (persisted in NoteDB), before
+ * {@link UUID} was introduced and does not apply to removals ( represented as approval with
+ * {@link #value}, set to '0').
+ */
+ public abstract Builder uuid(Optional<UUID> uuid);
+
+ public abstract Builder uuid(UUID uuid);
+
public abstract Builder value(short value);
public Builder value(int value) {
return value(Shorts.checkedCast(value));
}
- public abstract Builder granted(Timestamp granted);
-
- public Builder granted(Date granted) {
- return granted(new Timestamp(granted.getTime()));
- }
+ public abstract Builder granted(Instant granted);
public abstract Builder tag(String tag);
@@ -86,6 +122,8 @@
public abstract Key key();
+ public abstract Optional<UUID> uuid();
+
/**
* Value assigned by the user.
*
@@ -104,7 +142,7 @@
*/
public abstract short value();
- public abstract Timestamp granted();
+ public abstract Instant granted();
public abstract Optional<String> tag();
@@ -117,8 +155,24 @@
public abstract Builder toBuilder();
+ /**
+ * Makes a copy of {@link PatchSetApproval} that applies to {@code psId}.
+ *
+ * <p>The returned {@link PatchSetApproval} has the same {@link UUID} as the original {@link
+ * PatchSetApproval}, which is generated when it is originally granted.
+ *
+ * <p>This is needed since we want to keep the link between the original {@link PatchSetApproval}
+ * and the {@link #copied} one.
+ *
+ * @param psId {@link PatchSet.Id} of {@link PatchSet} that the copy should be applied to.
+ * @return {@link #copied} {@link PatchSetApproval} that applies to {@code psId}.
+ */
public PatchSetApproval copyWithPatchSet(PatchSet.Id psId) {
- return toBuilder().key(key(psId, key().accountId(), key().labelId())).copied(true).build();
+ return toBuilder()
+ .key(key(psId, key().accountId(), key().labelId()))
+ .uuid(uuid())
+ .copied(true)
+ .build();
}
public PatchSet.Id patchSetId() {
diff --git a/java/com/google/gerrit/entities/PatchSetInfo.java b/java/com/google/gerrit/entities/PatchSetInfo.java
index e3c6613..5770a7e 100644
--- a/java/com/google/gerrit/entities/PatchSetInfo.java
+++ b/java/com/google/gerrit/entities/PatchSetInfo.java
@@ -31,33 +31,33 @@
this.shortMessage = requireNonNull(shortMessage);
}
- protected ParentInfo() {}
+ ParentInfo() {}
}
- protected PatchSet.Id key;
+ private PatchSet.Id key;
/** First line of {@link #message}. */
- protected String subject;
+ private String subject;
/** The complete description of the change the patch set introduces. */
- protected String message;
+ private String message;
/** Identity of who wrote the patch set. May differ from {@link #committer}. */
- protected UserIdentity author;
+ private UserIdentity author;
/** Identity of who committed the patch set to the VCS. */
- protected UserIdentity committer;
+ private UserIdentity committer;
/** List of parents of the patch set. */
- protected List<ParentInfo> parents;
+ private List<ParentInfo> parents;
/** ID of commit. */
- protected ObjectId commitId;
+ private ObjectId commitId;
/** Optional user-supplied description for the patch set. */
- protected String description;
+ private String description;
- protected PatchSetInfo() {}
+ PatchSetInfo() {}
public PatchSetInfo(PatchSet.Id k) {
key = k;
diff --git a/java/com/google/gerrit/entities/RefNames.java b/java/com/google/gerrit/entities/RefNames.java
index 2263aba..b9c1b3c 100644
--- a/java/com/google/gerrit/entities/RefNames.java
+++ b/java/com/google/gerrit/entities/RefNames.java
@@ -14,8 +14,10 @@
package com.google.gerrit.entities;
+import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.common.UsedAt;
+import java.util.List;
/** Constants and utilities for Gerrit-specific ref names. */
public class RefNames {
@@ -126,6 +128,8 @@
REFS_STARRED_CHANGES,
REFS_REJECT_COMMITS);
+ private static final Splitter SPLITTER = Splitter.on("/");
+
public static String fullName(String ref) {
return (ref.startsWith(REFS) || ref.equals(HEAD)) ? ref : REFS_HEADS + ref;
}
@@ -344,16 +348,16 @@
return null;
}
- String[] parts = name.split("/");
- int n = parts.length;
+ List<String> parts = SPLITTER.splitToList(name);
+ int n = parts.size();
if (n < 2) {
return null;
}
// Last 2 digits.
int le;
- for (le = 0; le < parts[0].length(); le++) {
- if (!Character.isDigit(parts[0].charAt(le))) {
+ for (le = 0; le < parts.get(0).length(); le++) {
+ if (!Character.isDigit(parts.get(0).charAt(le))) {
return null;
}
}
@@ -363,8 +367,8 @@
// Full ID.
int ie;
- for (ie = 0; ie < parts[1].length(); ie++) {
- if (!Character.isDigit(parts[1].charAt(ie))) {
+ for (ie = 0; ie < parts.get(1).length(); ie++) {
+ if (!Character.isDigit(parts.get(1).charAt(ie))) {
if (ie == 0) {
return null;
}
@@ -372,8 +376,8 @@
}
}
- int shard = Integer.parseInt(parts[0]);
- int id = Integer.parseInt(parts[1].substring(0, ie));
+ int shard = Integer.parseInt(parts.get(0));
+ int id = Integer.parseInt(parts.get(1).substring(0, ie));
if (id % 100 != shard) {
return null;
@@ -387,20 +391,20 @@
return null;
}
- String[] parts = name.split("/");
- int n = parts.length;
+ List<String> parts = SPLITTER.splitToList(name);
+ int n = parts.size();
if (n != 2) {
return null;
}
// First 2 chars.
- if (parts[0].length() != 2) {
+ if (parts.get(0).length() != 2) {
return null;
}
// Full UUID.
- String uuid = parts[1];
- if (!uuid.startsWith(parts[0])) {
+ String uuid = parts.get(1);
+ if (!uuid.startsWith(parts.get(0))) {
return null;
}
@@ -421,16 +425,16 @@
return null;
}
- String[] parts = name.split("/");
- int n = parts.length;
+ List<String> parts = SPLITTER.splitToList(name);
+ int n = parts.size();
if (n < 2) {
return null;
}
// Last 2 digits.
int le;
- for (le = 0; le < parts[0].length(); le++) {
- if (!Character.isDigit(parts[0].charAt(le))) {
+ for (le = 0; le < parts.get(0).length(); le++) {
+ if (!Character.isDigit(parts.get(0).charAt(le))) {
return null;
}
}
@@ -440,8 +444,8 @@
// Full ID.
int ie;
- for (ie = 0; ie < parts[1].length(); ie++) {
- if (!Character.isDigit(parts[1].charAt(ie))) {
+ for (ie = 0; ie < parts.get(1).length(); ie++) {
+ if (!Character.isDigit(parts.get(1).charAt(ie))) {
if (ie == 0) {
return null;
}
@@ -449,8 +453,8 @@
}
}
- int shard = Integer.parseInt(parts[0]);
- int id = Integer.parseInt(parts[1].substring(0, ie));
+ int shard = Integer.parseInt(parts.get(0));
+ int id = Integer.parseInt(parts.get(1).substring(0, ie));
if (id % 100 != shard) {
return null;
@@ -489,7 +493,7 @@
return Integer.parseInt(rest.substring(0, ie));
}
- static Integer parseRefSuffix(String name) {
+ public static Integer parseRefSuffix(String name) {
if (name == null) {
return null;
}
diff --git a/java/com/google/gerrit/entities/RobotComment.java b/java/com/google/gerrit/entities/RobotComment.java
index e2e4114..1d46d3b 100644
--- a/java/com/google/gerrit/entities/RobotComment.java
+++ b/java/com/google/gerrit/entities/RobotComment.java
@@ -14,7 +14,7 @@
package com.google.gerrit.entities;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -29,7 +29,7 @@
public RobotComment(
Key key,
Account.Id author,
- Timestamp writtenOn,
+ Instant writtenOn,
short side,
String message,
String serverId,
diff --git a/java/com/google/gerrit/entities/SubmitRequirement.java b/java/com/google/gerrit/entities/SubmitRequirement.java
index 13e0b53..3f91cc7 100644
--- a/java/com/google/gerrit/entities/SubmitRequirement.java
+++ b/java/com/google/gerrit/entities/SubmitRequirement.java
@@ -15,11 +15,23 @@
package com.google.gerrit.entities;
import com.google.auto.value.AutoValue;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import java.util.Optional;
-/** Entity describing a requirement that should be met for a change to become submittable. */
+/**
+ * Entity describing a requirement that should be met for a change to become submittable.
+ *
+ * <p>There are two ways to contribute {@link SubmitRequirement}:
+ *
+ * <ul>
+ * <li>Set per-project in project.config (see {@link
+ * com.google.gerrit.server.project.ProjectState#getSubmitRequirements()}
+ * <li>Bind a global {@link SubmitRequirement} that will be evaluated for all projects.
+ * </ul>
+ */
+@ExtensionPoint
@AutoValue
public abstract class SubmitRequirement {
/** Requirement name. */
@@ -56,7 +68,12 @@
/**
* Boolean value indicating if the {@link SubmitRequirement} definition can be overridden in child
- * projects. Default is false.
+ * projects.
+ *
+ * <p>For globally bound {@link SubmitRequirement}, indicates if can be overridden by {@link
+ * SubmitRequirement} in project.config.
+ *
+ * <p>Default is false.
*/
public abstract boolean allowOverrideInChildProjects();
diff --git a/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java b/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java
index 900b2e2..aff0994 100644
--- a/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java
+++ b/java/com/google/gerrit/entities/SubmitRequirementExpressionResult.java
@@ -39,17 +39,17 @@
/**
* List leaf predicates that are fulfilled, for example the expression
*
- * <p><i>label:code-review=+2 and branch:refs/heads/master</i>
+ * <p><i>label:Code-Review=+2 and branch:refs/heads/master</i>
*
* <p>has two leaf predicates:
*
* <ul>
- * <li>label:code-review=+2
+ * <li>label:Code-Review=+2
* <li>branch:refs/heads/master
* </ul>
*
* This method will return the leaf predicates that were fulfilled, for example if only the first
- * predicate was fulfilled, the returned list will be equal to ["label:code-review=+2"].
+ * predicate was fulfilled, the returned list will be equal to ["label:Code-Review=+2"].
*/
public abstract ImmutableList<String> passingAtoms();
@@ -72,8 +72,17 @@
Status status,
ImmutableList<String> passingAtoms,
ImmutableList<String> failingAtoms) {
+ return create(expression, status, passingAtoms, failingAtoms, Optional.empty());
+ }
+
+ public static SubmitRequirementExpressionResult create(
+ SubmitRequirementExpression expression,
+ Status status,
+ ImmutableList<String> passingAtoms,
+ ImmutableList<String> failingAtoms,
+ Optional<String> errorMessage) {
return new AutoValue_SubmitRequirementExpressionResult(
- expression, status, Optional.empty(), passingAtoms, failingAtoms);
+ expression, status, errorMessage, passingAtoms, failingAtoms);
}
public static SubmitRequirementExpressionResult error(
diff --git a/java/com/google/gerrit/entities/SubmitRequirementResult.java b/java/com/google/gerrit/entities/SubmitRequirementResult.java
index 13625c1..c81b43f 100644
--- a/java/com/google/gerrit/entities/SubmitRequirementResult.java
+++ b/java/com/google/gerrit/entities/SubmitRequirementResult.java
@@ -53,9 +53,41 @@
return legacy().orElse(false);
}
+ /**
+ * Boolean indicating if the "submit requirement" was bypassed during submission, e.g. by
+ * performing a push with the %submit option.
+ */
+ public abstract Optional<Boolean> forced();
+
+ public Optional<String> errorMessage() {
+ if (!status().equals(Status.ERROR)) {
+ return Optional.empty();
+ }
+ if (applicabilityExpressionResult().isPresent()
+ && applicabilityExpressionResult().get().errorMessage().isPresent()) {
+ return Optional.of(
+ "Applicability expression result has an error: "
+ + applicabilityExpressionResult().get().errorMessage().get());
+ }
+ if (submittabilityExpressionResult().errorMessage().isPresent()) {
+ return Optional.of(
+ "Submittability expression result has an error: "
+ + submittabilityExpressionResult().errorMessage().get());
+ }
+ if (overrideExpressionResult().isPresent()
+ && overrideExpressionResult().get().errorMessage().isPresent()) {
+ return Optional.of(
+ "Override expression result has an error: "
+ + overrideExpressionResult().get().errorMessage().get());
+ }
+ return Optional.of("No error logged.");
+ }
+
@Memoized
public Status status() {
- if (assertError(submittabilityExpressionResult())
+ if (forced().orElse(false)) {
+ return Status.FORCED;
+ } else if (assertError(submittabilityExpressionResult())
|| assertError(applicabilityExpressionResult())
|| assertError(overrideExpressionResult())) {
return Status.ERROR;
@@ -74,13 +106,18 @@
@Memoized
public boolean fulfilled() {
Status s = status();
- return s == Status.SATISFIED || s == Status.OVERRIDDEN || s == Status.NOT_APPLICABLE;
+ return s == Status.SATISFIED
+ || s == Status.OVERRIDDEN
+ || s == Status.NOT_APPLICABLE
+ || s == Status.FORCED;
}
public static Builder builder() {
return new AutoValue_SubmitRequirementResult.Builder();
}
+ public abstract Builder toBuilder();
+
public static TypeAdapter<SubmitRequirementResult> typeAdapter(Gson gson) {
return new AutoValue_SubmitRequirementResult.GsonTypeAdapter(gson);
}
@@ -108,10 +145,16 @@
NOT_APPLICABLE,
/**
- * Any of the applicability, blocking or override expressions contain invalid syntax and are not
- * parsable.
+ * Any of the applicability, submittability or override expressions contain invalid syntax and
+ * are not parsable.
*/
- ERROR
+ ERROR,
+
+ /**
+ * The "submit requirement" was bypassed during submission, e.g. by pushing for review with the
+ * %submit option.
+ */
+ FORCED
}
@AutoValue.Builder
@@ -130,6 +173,8 @@
public abstract Builder legacy(Optional<Boolean> value);
+ public abstract Builder forced(Optional<Boolean> value);
+
public abstract SubmitRequirementResult build();
}
diff --git a/java/com/google/gerrit/entities/SubmoduleSubscription.java b/java/com/google/gerrit/entities/SubmoduleSubscription.java
index 5ea1b1e..db26eb3 100644
--- a/java/com/google/gerrit/entities/SubmoduleSubscription.java
+++ b/java/com/google/gerrit/entities/SubmoduleSubscription.java
@@ -25,11 +25,11 @@
* <p>A subscriber operates a submodule in defined path.
*/
public final class SubmoduleSubscription {
- protected BranchNameKey superProject;
+ private BranchNameKey superProject;
- protected String submodulePath;
+ private String submodulePath;
- protected BranchNameKey submodule;
+ private BranchNameKey submodule;
public SubmoduleSubscription(BranchNameKey superProject, BranchNameKey submodule, String path) {
this.superProject = superProject;
diff --git a/java/com/google/gerrit/entities/UserIdentity.java b/java/com/google/gerrit/entities/UserIdentity.java
index e07d21a..8334157 100644
--- a/java/com/google/gerrit/entities/UserIdentity.java
+++ b/java/com/google/gerrit/entities/UserIdentity.java
@@ -14,26 +14,26 @@
package com.google.gerrit.entities;
-import java.sql.Timestamp;
+import java.time.Instant;
public final class UserIdentity {
/** Full name of the user. */
- protected String name;
+ private String name;
/** Email address (or user@host style string anyway). */
- protected String email;
+ private String email;
/** Username of the user. */
- protected String username;
+ private String username;
/** Time (in UTC) when the identity was constructed. */
- protected Timestamp when;
+ private Instant when;
/** Offset from UTC */
- protected int tz;
+ private int tz;
/** If the user has a Gerrit account, their account identity. */
- protected Account.Id accountId;
+ private Account.Id accountId;
public String getName() {
return name;
@@ -55,11 +55,11 @@
return username;
}
- public Timestamp getDate() {
+ public Instant getDate() {
return when;
}
- public void setDate(Timestamp d) {
+ public void setDate(Instant d) {
when = d;
}
diff --git a/java/com/google/gerrit/entities/converter/ChangeMessageProtoConverter.java b/java/com/google/gerrit/entities/converter/ChangeMessageProtoConverter.java
index eb2a381..edf921e 100644
--- a/java/com/google/gerrit/entities/converter/ChangeMessageProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ChangeMessageProtoConverter.java
@@ -20,7 +20,7 @@
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.proto.Entities;
import com.google.protobuf.Parser;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Objects;
@Immutable
@@ -44,9 +44,9 @@
if (author != null) {
builder.setAuthorId(accountIdConverter.toProto(author));
}
- Timestamp writtenOn = changeMessage.getWrittenOn();
+ Instant writtenOn = changeMessage.getWrittenOn();
if (writtenOn != null) {
- builder.setWrittenOn(writtenOn.getTime());
+ builder.setWrittenOn(writtenOn.toEpochMilli());
}
// Build proto with template representation of the message. Templates are parsed when message is
// extracted from cache.
@@ -78,7 +78,7 @@
proto.hasKey() ? changeMessageKeyConverter.fromProto(proto.getKey()) : null;
Account.Id author =
proto.hasAuthorId() ? accountIdConverter.fromProto(proto.getAuthorId()) : null;
- Timestamp writtenOn = proto.hasWrittenOn() ? new Timestamp(proto.getWrittenOn()) : null;
+ Instant writtenOn = proto.hasWrittenOn() ? Instant.ofEpochMilli(proto.getWrittenOn()) : null;
PatchSet.Id patchSetId =
proto.hasPatchset() ? patchSetIdConverter.fromProto(proto.getPatchset()) : null;
// Only template representation of the message is stored in entity. Templates should be replaced
diff --git a/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java b/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
index 689b4aa..4903364 100644
--- a/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
@@ -21,7 +21,7 @@
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.proto.Entities;
import com.google.protobuf.Parser;
-import java.sql.Timestamp;
+import java.time.Instant;
@Immutable
public enum ChangeProtoConverter implements ProtoConverter<Entities.Change, Change> {
@@ -44,8 +44,8 @@
Entities.Change.newBuilder()
.setChangeId(changeIdConverter.toProto(change.getId()))
.setChangeKey(changeKeyConverter.toProto(change.getKey()))
- .setCreatedOn(change.getCreatedOn().getTime())
- .setLastUpdatedOn(change.getLastUpdatedOn().getTime())
+ .setCreatedOn(change.getCreatedOn().toEpochMilli())
+ .setLastUpdatedOn(change.getLastUpdatedOn().toEpochMilli())
.setOwnerAccountId(accountIdConverter.toProto(change.getOwner()))
.setDest(branchNameConverter.toProto(change.getDest()))
.setStatus(change.getStatus().getCode())
@@ -96,9 +96,9 @@
BranchNameKey destination =
proto.hasDest() ? branchNameConverter.fromProto(proto.getDest()) : null;
Change change =
- new Change(key, changeId, owner, destination, new Timestamp(proto.getCreatedOn()));
+ new Change(key, changeId, owner, destination, Instant.ofEpochMilli(proto.getCreatedOn()));
if (proto.hasLastUpdatedOn()) {
- change.setLastUpdatedOn(new Timestamp(proto.getLastUpdatedOn()));
+ change.setLastUpdatedOn(Instant.ofEpochMilli(proto.getLastUpdatedOn()));
}
Change.Status status = Change.Status.forCode((char) proto.getStatus());
if (status != null) {
diff --git a/java/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverter.java b/java/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverter.java
index 9e77025..e8ef346 100644
--- a/java/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverter.java
@@ -19,7 +19,7 @@
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.proto.Entities;
import com.google.protobuf.Parser;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Objects;
@Immutable
@@ -38,10 +38,11 @@
Entities.PatchSetApproval.newBuilder()
.setKey(patchSetApprovalKeyProtoConverter.toProto(patchSetApproval.key()))
.setValue(patchSetApproval.value())
- .setGranted(patchSetApproval.granted().getTime())
+ .setGranted(patchSetApproval.granted().toEpochMilli())
.setPostSubmit(patchSetApproval.postSubmit())
.setCopied(patchSetApproval.copied());
+ patchSetApproval.uuid().ifPresent(uuid -> builder.setUuid(uuid.get()));
patchSetApproval.tag().ifPresent(builder::setTag);
Account.Id realAccountId = patchSetApproval.realAccountId();
// PatchSetApproval#getRealAccountId automatically delegates to PatchSetApproval#getAccountId if
@@ -61,9 +62,12 @@
PatchSetApproval.builder()
.key(patchSetApprovalKeyProtoConverter.fromProto(proto.getKey()))
.value(proto.getValue())
- .granted(new Timestamp(proto.getGranted()))
+ .granted(Instant.ofEpochMilli(proto.getGranted()))
.postSubmit(proto.getPostSubmit())
.copied(proto.getCopied());
+ if (proto.hasUuid()) {
+ builder.uuid(PatchSetApproval.uuid(proto.getUuid()));
+ }
if (proto.hasTag()) {
builder.tag(proto.getTag());
}
diff --git a/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java b/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java
index 13a6e71..210972d 100644
--- a/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java
@@ -20,7 +20,7 @@
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.proto.Entities;
import com.google.protobuf.Parser;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.List;
import org.eclipse.jgit.lib.ObjectId;
@@ -42,7 +42,7 @@
.setId(patchSetIdConverter.toProto(patchSet.id()))
.setCommitId(objectIdConverter.toProto(patchSet.commitId()))
.setUploaderAccountId(accountIdConverter.toProto(patchSet.uploader()))
- .setCreatedOn(patchSet.createdOn().getTime());
+ .setCreatedOn(patchSet.createdOn().toEpochMilli());
List<String> groups = patchSet.groups();
if (!groups.isEmpty()) {
builder.setGroups(PatchSet.joinGroups(groups));
@@ -84,7 +84,8 @@
proto.hasUploaderAccountId()
? accountIdConverter.fromProto(proto.getUploaderAccountId())
: Account.id(0))
- .createdOn(proto.hasCreatedOn() ? new Timestamp(proto.getCreatedOn()) : new Timestamp(0));
+ .createdOn(
+ proto.hasCreatedOn() ? Instant.ofEpochMilli(proto.getCreatedOn()) : Instant.EPOCH);
return builder.build();
}
diff --git a/java/com/google/gerrit/exceptions/InternalServerWithUserMessageException.java b/java/com/google/gerrit/exceptions/MergeUpdateException.java
similarity index 64%
rename from java/com/google/gerrit/exceptions/InternalServerWithUserMessageException.java
rename to java/com/google/gerrit/exceptions/MergeUpdateException.java
index 452192c..b60ca57 100644
--- a/java/com/google/gerrit/exceptions/InternalServerWithUserMessageException.java
+++ b/java/com/google/gerrit/exceptions/MergeUpdateException.java
@@ -14,10 +14,15 @@
package com.google.gerrit.exceptions;
-public class InternalServerWithUserMessageException extends RuntimeException {
+/**
+ * An exception used for changes that fail to merge. This exception has a user visible message
+ * unlike other {@link RuntimeException}s, because this is our way to improve the UX when
+ * submission/merges fail.
+ */
+public class MergeUpdateException extends RuntimeException {
private static final long serialVersionUID = 1L;
- public InternalServerWithUserMessageException(String msg, Throwable cause) {
- super(msg, cause);
+ public MergeUpdateException(String userVisibleMessage, Throwable cause) {
+ super(userVisibleMessage, cause);
}
}
diff --git a/java/com/google/gerrit/extensions/api/changes/DeleteVoteInput.java b/java/com/google/gerrit/extensions/api/changes/DeleteVoteInput.java
index ee10a1d..8432c8f 100644
--- a/java/com/google/gerrit/extensions/api/changes/DeleteVoteInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/DeleteVoteInput.java
@@ -25,4 +25,10 @@
public NotifyHandling notify = NotifyHandling.ALL;
public Map<RecipientType, NotifyInfo> notifyDetails;
+
+ /**
+ * Users in the attention set will not be added/removed from this endpoint call. Normally, users
+ * are added to the attention set upon deletion of their vote by other users.
+ */
+ public boolean ignoreAutomaticAttentionSetRules;
}
diff --git a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 1307516..b659cca 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -51,12 +51,6 @@
ChangeInfo submit(SubmitInput in) throws RestApiException;
- default BinaryResult submitPreview() throws RestApiException {
- return submitPreview("zip");
- }
-
- BinaryResult submitPreview(String format) throws RestApiException;
-
ChangeApi cherryPick(CherryPickInput in) throws RestApiException;
ChangeInfo cherryPickAsInfo(CherryPickInput in) throws RestApiException;
@@ -369,11 +363,6 @@
}
@Override
- public BinaryResult submitPreview(String format) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
public SubmitType testSubmitType(TestSubmitRuleInput in) throws RestApiException {
throw new NotImplementedException();
}
diff --git a/java/com/google/gerrit/extensions/api/changes/SubmittedTogetherOption.java b/java/com/google/gerrit/extensions/api/changes/SubmittedTogetherOption.java
index e2cab4d..68a4e88 100644
--- a/java/com/google/gerrit/extensions/api/changes/SubmittedTogetherOption.java
+++ b/java/com/google/gerrit/extensions/api/changes/SubmittedTogetherOption.java
@@ -16,5 +16,6 @@
/** Output options available for submitted_together requests. */
public enum SubmittedTogetherOption {
- NON_VISIBLE_CHANGES;
+ NON_VISIBLE_CHANGES,
+ TOPIC_CLOSURE;
}
diff --git a/java/com/google/gerrit/extensions/api/projects/TagInfo.java b/java/com/google/gerrit/extensions/api/projects/TagInfo.java
index a6269fe..61ea518 100644
--- a/java/com/google/gerrit/extensions/api/projects/TagInfo.java
+++ b/java/com/google/gerrit/extensions/api/projects/TagInfo.java
@@ -14,16 +14,22 @@
package com.google.gerrit.extensions.api.projects;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.common.GitPerson;
import com.google.gerrit.extensions.common.WebLinkInfo;
import java.sql.Timestamp;
+import java.time.Instant;
import java.util.List;
public class TagInfo extends RefInfo {
public String object;
public String message;
public GitPerson tagger;
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
public Timestamp created;
+
public List<WebLinkInfo> webLinks;
public TagInfo(
@@ -39,8 +45,22 @@
this.created = created;
}
+ @SuppressWarnings("JdkObsolete")
+ public TagInfo(
+ String ref,
+ String revision,
+ Boolean canDelete,
+ List<WebLinkInfo> webLinks,
+ @Nullable Instant created) {
+ this.ref = ref;
+ this.revision = revision;
+ this.canDelete = canDelete;
+ this.webLinks = webLinks;
+ this.created = created != null ? Timestamp.from(created) : null;
+ }
+
public TagInfo(String ref, String revision, Boolean canDelete, List<WebLinkInfo> webLinks) {
- this(ref, revision, canDelete, webLinks, null);
+ this(ref, revision, canDelete, webLinks, (Instant) null);
}
public TagInfo(
@@ -66,8 +86,24 @@
String message,
GitPerson tagger,
Boolean canDelete,
+ List<WebLinkInfo> webLinks,
+ Instant created) {
+ this(ref, revision, canDelete, webLinks, created);
+ this.object = object;
+ this.message = message;
+ this.tagger = tagger;
+ this.webLinks = webLinks;
+ }
+
+ public TagInfo(
+ String ref,
+ String revision,
+ String object,
+ String message,
+ GitPerson tagger,
+ Boolean canDelete,
List<WebLinkInfo> webLinks) {
- this(ref, revision, object, message, tagger, canDelete, webLinks, null);
+ this(ref, revision, object, message, tagger, canDelete, webLinks, (Instant) null);
this.object = object;
this.message = message;
this.tagger = tagger;
diff --git a/java/com/google/gerrit/extensions/client/Comment.java b/java/com/google/gerrit/extensions/client/Comment.java
index 634992e..b8843d3 100644
--- a/java/com/google/gerrit/extensions/client/Comment.java
+++ b/java/com/google/gerrit/extensions/client/Comment.java
@@ -15,6 +15,7 @@
package com.google.gerrit.extensions.client;
import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Comparator;
import java.util.Objects;
@@ -35,7 +36,11 @@
public Range range;
public String inReplyTo;
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
public Timestamp updated;
+
public String message;
/**
@@ -44,6 +49,20 @@
*/
public String commitId;
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
+ public Instant getUpdated() {
+ return updated.toInstant();
+ }
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
+ public void setUpdated(Instant when) {
+ updated = Timestamp.from(when);
+ }
+
public static class Range implements Comparable<Range> {
private static final Comparator<Range> RANGE_COMPARATOR =
Comparator.<Range>comparingInt(range -> range.startLine)
@@ -70,10 +89,10 @@
public boolean equals(Object o) {
if (o instanceof Range) {
Range r = (Range) o;
- return Objects.equals(startLine, r.startLine)
- && Objects.equals(startCharacter, r.startCharacter)
- && Objects.equals(endLine, r.endLine)
- && Objects.equals(endCharacter, r.endCharacter);
+ return startLine == r.startLine
+ && startCharacter == r.startCharacter
+ && endLine == r.endLine
+ && endCharacter == r.endCharacter;
}
return false;
}
@@ -110,6 +129,11 @@
return 1;
}
+ // This is a value class that allows adding attributes by subclassing.
+ // Doing this is discouraged and using composition rather than inheritance to add fields to value
+ // types is preferred. However this class is part of the extension API, hence we cannot change it
+ // without breaking the API. Hence suppress the EqualsGetClass warning here.
+ @SuppressWarnings("EqualsGetClass")
@Override
public boolean equals(Object o) {
if (this == o) {
diff --git a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
index b26f435..6acf3f4 100644
--- a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
+++ b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
@@ -22,9 +22,6 @@
/** Default number of items to display per page. */
public static final int DEFAULT_PAGESIZE = 25;
- /** Valid choices for the page size. */
- public static final int[] PAGESIZE_CHOICES = {10, 25, 50, 100};
-
/** Preferred method to download a change. */
public enum DownloadCommand {
PULL,
diff --git a/java/com/google/gerrit/extensions/common/AccountDetailInfo.java b/java/com/google/gerrit/extensions/common/AccountDetailInfo.java
index a2aeab2..a76a7f9 100644
--- a/java/com/google/gerrit/extensions/common/AccountDetailInfo.java
+++ b/java/com/google/gerrit/extensions/common/AccountDetailInfo.java
@@ -15,6 +15,7 @@
package com.google.gerrit.extensions.common;
import java.sql.Timestamp;
+import java.time.Instant;
/**
* Representation of a (detailed) account in the REST API.
@@ -27,9 +28,18 @@
*/
public class AccountDetailInfo extends AccountInfo {
/** The timestamp of when the account was registered. */
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
public Timestamp registeredOn;
public AccountDetailInfo(Integer id) {
super(id);
}
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
+ public void setRegisteredOn(Instant registeredOn) {
+ this.registeredOn = Timestamp.from(registeredOn);
+ }
}
diff --git a/java/com/google/gerrit/extensions/common/AccountExternalIdInfo.java b/java/com/google/gerrit/extensions/common/AccountExternalIdInfo.java
index e3e0fc8..b51e195 100644
--- a/java/com/google/gerrit/extensions/common/AccountExternalIdInfo.java
+++ b/java/com/google/gerrit/extensions/common/AccountExternalIdInfo.java
@@ -57,10 +57,10 @@
public boolean equals(Object o) {
if (o instanceof AccountExternalIdInfo) {
AccountExternalIdInfo a = (AccountExternalIdInfo) o;
- return (Objects.equals(a.identity, identity))
- && (Objects.equals(a.emailAddress, emailAddress))
- && (Objects.equals(a.trusted, trusted))
- && (Objects.equals(a.canDelete, canDelete));
+ return Objects.equals(a.identity, identity)
+ && Objects.equals(a.emailAddress, emailAddress)
+ && Objects.equals(a.trusted, trusted)
+ && Objects.equals(a.canDelete, canDelete);
}
return false;
}
diff --git a/java/com/google/gerrit/extensions/common/ApprovalInfo.java b/java/com/google/gerrit/extensions/common/ApprovalInfo.java
index bf72e83..4519add 100644
--- a/java/com/google/gerrit/extensions/common/ApprovalInfo.java
+++ b/java/com/google/gerrit/extensions/common/ApprovalInfo.java
@@ -16,6 +16,7 @@
import com.google.gerrit.common.Nullable;
import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Objects;
/**
@@ -43,6 +44,8 @@
public Integer value;
/** The time and date describing when the approval was made. */
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
public Timestamp date;
/** Whether this vote was made after the change was submitted. */
@@ -62,10 +65,10 @@
public ApprovalInfo(
Integer id,
- Integer value,
+ @Nullable Integer value,
@Nullable VotingRangeInfo permittedVotingRange,
@Nullable String tag,
- Timestamp date) {
+ @Nullable Timestamp date) {
super(id);
this.value = value;
this.permittedVotingRange = permittedVotingRange;
@@ -73,6 +76,28 @@
this.tag = tag;
}
+ public ApprovalInfo(
+ Integer id,
+ @Nullable Integer value,
+ @Nullable VotingRangeInfo permittedVotingRange,
+ @Nullable String tag,
+ @Nullable Instant date) {
+ super(id);
+ this.value = value;
+ this.permittedVotingRange = permittedVotingRange;
+ this.tag = tag;
+ if (date != null) {
+ setDate(date);
+ }
+ }
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
+ public void setDate(Instant date) {
+ this.date = Timestamp.from(date);
+ }
+
@Override
public boolean equals(Object o) {
if (o instanceof ApprovalInfo) {
@@ -88,6 +113,11 @@
}
@Override
+ public String toString() {
+ return super.toString() + ", value=" + this.value;
+ }
+
+ @Override
public int hashCode() {
return Objects.hash(super.hashCode(), tag, value, date, postSubmit, permittedVotingRange);
}
diff --git a/java/com/google/gerrit/extensions/common/AttentionSetInfo.java b/java/com/google/gerrit/extensions/common/AttentionSetInfo.java
index d34ba6d..81dbc88 100644
--- a/java/com/google/gerrit/extensions/common/AttentionSetInfo.java
+++ b/java/com/google/gerrit/extensions/common/AttentionSetInfo.java
@@ -16,6 +16,7 @@
import com.google.gerrit.common.Nullable;
import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Objects;
/**
@@ -28,8 +29,12 @@
public class AttentionSetInfo {
/** The user included in the attention set. */
public AccountInfo account;
+
/** The timestamp of the last update. */
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
public Timestamp lastUpdate;
+
/** The human readable reason why the user was added. */
public String reason;
@@ -51,6 +56,17 @@
this.reasonAccount = reasonAccount;
}
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
+ public AttentionSetInfo(
+ AccountInfo account, Instant lastUpdate, String reason, @Nullable AccountInfo reasonAccount) {
+ this.account = account;
+ this.lastUpdate = Timestamp.from(lastUpdate);
+ this.reason = reason;
+ this.reasonAccount = reasonAccount;
+ }
+
protected AttentionSetInfo() {}
@Override
diff --git a/java/com/google/gerrit/extensions/common/BlameInfo.java b/java/com/google/gerrit/extensions/common/BlameInfo.java
index df3f373..6ee677e 100644
--- a/java/com/google/gerrit/extensions/common/BlameInfo.java
+++ b/java/com/google/gerrit/extensions/common/BlameInfo.java
@@ -28,7 +28,7 @@
if (this == o) {
return true;
}
- if (o == null || getClass() != o.getClass()) {
+ if (o == null || !(o instanceof BlameInfo)) {
return false;
}
BlameInfo blameInfo = (BlameInfo) o;
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfo.java b/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 2bb3dd7..40ae2ec 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -20,6 +20,7 @@
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.client.SubmitType;
import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -46,14 +47,20 @@
*/
public Map<Integer, AttentionSetInfo> attentionSet;
+ public Map<Integer, AttentionSetInfo> removedFromAttentionSet;
+
public AccountInfo assignee;
public Collection<String> hashtags;
public String changeId;
public String subject;
public ChangeStatus status;
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
public Timestamp created;
public Timestamp updated;
public Timestamp submitted;
+
public AccountInfo submitter;
public Boolean starred;
public Collection<String> stars;
@@ -124,4 +131,47 @@
public ChangeInfo(Map<String, RevisionInfo> revisions) {
this.revisions = ImmutableMap.copyOf(revisions);
}
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
+ public Instant getCreated() {
+ return created.toInstant();
+ }
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
+ public void setCreated(Instant when) {
+ created = Timestamp.from(when);
+ }
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
+ public Instant getUpdated() {
+ return updated.toInstant();
+ }
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
+ public void setUpdated(Instant when) {
+ updated = Timestamp.from(when);
+ }
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
+ public Instant getSubmitted() {
+ return submitted.toInstant();
+ }
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
+ public void setSubmitted(Instant when, AccountInfo who) {
+ submitted = Timestamp.from(when);
+ submitter = who;
+ }
}
diff --git a/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java b/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
index c1cb1627..51fe57c 100644
--- a/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
@@ -14,7 +14,9 @@
package com.google.gerrit.extensions.common;
+import com.google.common.collect.Iterables;
import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Collection;
import java.util.Objects;
@@ -24,7 +26,11 @@
public String tag;
public AccountInfo author;
public AccountInfo realAuthor;
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
public Timestamp date;
+
public String message;
public Collection<AccountInfo> accountsInMessage;
public Integer _revisionNumber;
@@ -35,6 +41,13 @@
this.message = message;
}
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
+ public void setDate(Instant when) {
+ date = Timestamp.from(when);
+ }
+
@Override
public boolean equals(Object o) {
if (o instanceof ChangeMessageInfo) {
@@ -45,7 +58,10 @@
&& Objects.equals(realAuthor, cmi.realAuthor)
&& Objects.equals(date, cmi.date)
&& Objects.equals(message, cmi.message)
- && Objects.equals(accountsInMessage, cmi.accountsInMessage)
+ && ((accountsInMessage == null && cmi.accountsInMessage == null)
+ || (accountsInMessage != null
+ && cmi.accountsInMessage != null
+ && Iterables.elementsEqual(accountsInMessage, cmi.accountsInMessage)))
&& Objects.equals(_revisionNumber, cmi._revisionNumber);
}
return false;
diff --git a/java/com/google/gerrit/extensions/common/GitPerson.java b/java/com/google/gerrit/extensions/common/GitPerson.java
index 8ed919e..df3e488 100644
--- a/java/com/google/gerrit/extensions/common/GitPerson.java
+++ b/java/com/google/gerrit/extensions/common/GitPerson.java
@@ -15,14 +15,26 @@
package com.google.gerrit.extensions.common;
import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Objects;
public class GitPerson {
public String name;
public String email;
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
public Timestamp date;
+
public int tz;
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
+ public void setDate(Instant when) {
+ date = Timestamp.from(when);
+ }
+
@Override
public boolean equals(Object o) {
if (!(o instanceof GitPerson)) {
diff --git a/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java b/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java
index 711337a..9a13713 100644
--- a/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java
+++ b/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java
@@ -14,7 +14,9 @@
package com.google.gerrit.extensions.common;
+import com.google.gerrit.common.Nullable;
import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Optional;
public abstract class GroupAuditEventInfo {
@@ -27,25 +29,62 @@
public Type type;
public AccountInfo user;
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
public Timestamp date;
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
public static UserMemberAuditEventInfo createAddUserEvent(
AccountInfo user, Timestamp date, AccountInfo member) {
- return new UserMemberAuditEventInfo(Type.ADD_USER, user, Optional.of(date), member);
+ return new UserMemberAuditEventInfo(Type.ADD_USER, user, date.toInstant(), member);
+ }
+
+ public static UserMemberAuditEventInfo createAddUserEvent(
+ AccountInfo user, Instant date, AccountInfo member) {
+ return new UserMemberAuditEventInfo(Type.ADD_USER, user, date, member);
+ }
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
+ public static UserMemberAuditEventInfo createRemoveUserEvent(
+ AccountInfo user, Optional<Timestamp> date, AccountInfo member) {
+ return new UserMemberAuditEventInfo(
+ Type.REMOVE_USER, user, date.map(Timestamp::toInstant).orElse(null), member);
}
public static UserMemberAuditEventInfo createRemoveUserEvent(
- AccountInfo user, Optional<Timestamp> date, AccountInfo member) {
+ AccountInfo user, @Nullable Instant date, AccountInfo member) {
return new UserMemberAuditEventInfo(Type.REMOVE_USER, user, date, member);
}
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
public static GroupMemberAuditEventInfo createAddGroupEvent(
AccountInfo user, Timestamp date, GroupInfo member) {
- return new GroupMemberAuditEventInfo(Type.ADD_GROUP, user, Optional.of(date), member);
+ return new GroupMemberAuditEventInfo(Type.ADD_GROUP, user, date.toInstant(), member);
+ }
+
+ public static GroupMemberAuditEventInfo createAddGroupEvent(
+ AccountInfo user, Instant date, GroupInfo member) {
+ return new GroupMemberAuditEventInfo(Type.ADD_GROUP, user, date, member);
+ }
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
+ public static GroupMemberAuditEventInfo createRemoveGroupEvent(
+ AccountInfo user, Optional<Timestamp> date, GroupInfo member) {
+ return new GroupMemberAuditEventInfo(
+ Type.REMOVE_GROUP, user, date.map(Timestamp::toInstant).orElse(null), member);
}
public static GroupMemberAuditEventInfo createRemoveGroupEvent(
- AccountInfo user, Optional<Timestamp> date, GroupInfo member) {
+ AccountInfo user, @Nullable Instant date, GroupInfo member) {
return new GroupMemberAuditEventInfo(Type.REMOVE_GROUP, user, date, member);
}
@@ -55,11 +94,20 @@
this.date = date.orElse(null);
}
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
+ protected GroupAuditEventInfo(Type type, AccountInfo user, @Nullable Instant date) {
+ this.type = type;
+ this.user = user;
+ this.date = date != null ? Timestamp.from(date) : null;
+ }
+
public static class UserMemberAuditEventInfo extends GroupAuditEventInfo {
public AccountInfo member;
private UserMemberAuditEventInfo(
- Type type, AccountInfo user, Optional<Timestamp> date, AccountInfo member) {
+ Type type, AccountInfo user, @Nullable Instant date, AccountInfo member) {
super(type, user, date);
this.member = member;
}
@@ -69,7 +117,7 @@
public GroupInfo member;
private GroupMemberAuditEventInfo(
- Type type, AccountInfo user, Optional<Timestamp> date, GroupInfo member) {
+ Type type, AccountInfo user, @Nullable Instant date, GroupInfo member) {
super(type, user, date);
this.member = member;
}
diff --git a/java/com/google/gerrit/extensions/common/GroupInfo.java b/java/com/google/gerrit/extensions/common/GroupInfo.java
index b21475c..edbaa01 100644
--- a/java/com/google/gerrit/extensions/common/GroupInfo.java
+++ b/java/com/google/gerrit/extensions/common/GroupInfo.java
@@ -15,6 +15,7 @@
package com.google.gerrit.extensions.common;
import java.sql.Timestamp;
+import java.time.Instant;
import java.util.List;
public class GroupInfo extends GroupBaseInfo {
@@ -26,10 +27,28 @@
public Integer groupId;
public String owner;
public String ownerId;
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
public Timestamp createdOn;
+
public Boolean _moreGroups;
// These fields are only supplied for internal groups, and only if requested.
public List<AccountInfo> members;
public List<GroupInfo> includes;
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
+ public Instant getCreatedOn() {
+ return createdOn.toInstant();
+ }
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
+ public void setCreatedOn(Instant when) {
+ createdOn = Timestamp.from(when);
+ }
}
diff --git a/java/com/google/gerrit/extensions/common/LabelDefinitionInfo.java b/java/com/google/gerrit/extensions/common/LabelDefinitionInfo.java
index 6f733d6..d3baecd 100644
--- a/java/com/google/gerrit/extensions/common/LabelDefinitionInfo.java
+++ b/java/com/google/gerrit/extensions/common/LabelDefinitionInfo.java
@@ -19,6 +19,7 @@
public class LabelDefinitionInfo {
public String name;
+ public String description;
public String projectName;
public String function;
public Map<String, String> values;
diff --git a/java/com/google/gerrit/extensions/common/LabelDefinitionInput.java b/java/com/google/gerrit/extensions/common/LabelDefinitionInput.java
index 38b76c1..1d580bb 100644
--- a/java/com/google/gerrit/extensions/common/LabelDefinitionInput.java
+++ b/java/com/google/gerrit/extensions/common/LabelDefinitionInput.java
@@ -19,6 +19,7 @@
public class LabelDefinitionInput extends InputWithCommitMessage {
public String name;
+ public String description;
public String function;
public Map<String, String> values;
public Short defaultValue;
diff --git a/java/com/google/gerrit/extensions/common/LabelInfo.java b/java/com/google/gerrit/extensions/common/LabelInfo.java
index 44bcdaf..cdd3b18 100644
--- a/java/com/google/gerrit/extensions/common/LabelInfo.java
+++ b/java/com/google/gerrit/extensions/common/LabelInfo.java
@@ -27,6 +27,7 @@
public Map<String, String> values;
+ public String description;
public Short value;
public Short defaultValue;
public Boolean optional;
diff --git a/java/com/google/gerrit/extensions/common/ReviewerUpdateInfo.java b/java/com/google/gerrit/extensions/common/ReviewerUpdateInfo.java
index 37e1ceb..36682f6 100644
--- a/java/com/google/gerrit/extensions/common/ReviewerUpdateInfo.java
+++ b/java/com/google/gerrit/extensions/common/ReviewerUpdateInfo.java
@@ -16,14 +16,31 @@
import com.google.gerrit.extensions.client.ReviewerState;
import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Objects;
public class ReviewerUpdateInfo {
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
public Timestamp updated;
+
public AccountInfo updatedBy;
public AccountInfo reviewer;
public ReviewerState state;
+ public ReviewerUpdateInfo() {}
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
+ public ReviewerUpdateInfo(
+ Instant updated, AccountInfo updatedBy, AccountInfo reviewer, ReviewerState state) {
+ this.updated = Timestamp.from(updated);
+ this.updatedBy = updatedBy;
+ this.reviewer = reviewer;
+ this.state = state;
+ }
+
@Override
public boolean equals(Object o) {
if (o instanceof ReviewerUpdateInfo) {
diff --git a/java/com/google/gerrit/extensions/common/RevisionInfo.java b/java/com/google/gerrit/extensions/common/RevisionInfo.java
index f710ab7..7c52c8c 100644
--- a/java/com/google/gerrit/extensions/common/RevisionInfo.java
+++ b/java/com/google/gerrit/extensions/common/RevisionInfo.java
@@ -16,6 +16,7 @@
import com.google.gerrit.extensions.client.ChangeKind;
import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Map;
import java.util.Objects;
@@ -25,7 +26,11 @@
public transient boolean isCurrent;
public ChangeKind kind;
public int _number;
+
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
public Timestamp created;
+
public AccountInfo uploader;
public String ref;
public Map<String, FetchInfo> fetch;
@@ -51,6 +56,13 @@
this.uploader = uploader;
}
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
+ public void setCreated(Instant date) {
+ this.created = Timestamp.from(date);
+ }
+
@Override
public boolean equals(Object o) {
if (o instanceof RevisionInfo) {
diff --git a/java/com/google/gerrit/extensions/common/SubmitRequirementExpressionInfo.java b/java/com/google/gerrit/extensions/common/SubmitRequirementExpressionInfo.java
index 4d1fce2..e9549c9 100644
--- a/java/com/google/gerrit/extensions/common/SubmitRequirementExpressionInfo.java
+++ b/java/com/google/gerrit/extensions/common/SubmitRequirementExpressionInfo.java
@@ -36,4 +36,10 @@
* has two atoms: ["branch:refs/heads/foo", "project:bar"].
*/
public List<String> failingAtoms;
+
+ /**
+ * Optional error message. Contains an explanation of why the submit requirement expression failed
+ * during its evaluation.
+ */
+ public String errorMessage;
}
diff --git a/java/com/google/gerrit/extensions/common/SubmitRequirementResultInfo.java b/java/com/google/gerrit/extensions/common/SubmitRequirementResultInfo.java
index 3d50f13..7b87be8 100644
--- a/java/com/google/gerrit/extensions/common/SubmitRequirementResultInfo.java
+++ b/java/com/google/gerrit/extensions/common/SubmitRequirementResultInfo.java
@@ -41,7 +41,13 @@
* Any of the applicability, submittability or override expressions contain invalid syntax and
* are not parsable.
*/
- ERROR
+ ERROR,
+
+ /**
+ * The "submit requirement" was bypassed during submission, e.g. by pushing for review with the
+ * %submit option.
+ */
+ FORCED
}
/** Submit requirement name. */
diff --git a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
index d827d5d..cb9d855 100644
--- a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
@@ -24,7 +24,6 @@
import com.google.common.truth.Subject;
import com.google.gerrit.extensions.common.GitPerson;
import java.sql.Timestamp;
-import java.util.Date;
import org.eclipse.jgit.lib.PersonIdent;
public class GitPersonSubject extends Subject {
@@ -71,11 +70,16 @@
tz().isEqualTo(other.tz);
}
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
public void matches(PersonIdent ident) {
isNotNull();
name().isEqualTo(ident.getName());
email().isEqualTo(ident.getEmailAddress());
- check("roundedDate()").that(new Date(gitPerson.date.getTime())).isEqualTo(ident.getWhen());
+ check("roundedDate()").that(gitPerson.date.getTime()).isEqualTo(ident.getWhen().getTime());
tz().isEqualTo(ident.getTimeZoneOffset());
}
}
diff --git a/java/com/google/gerrit/extensions/events/ChangeEvent.java b/java/com/google/gerrit/extensions/events/ChangeEvent.java
index def75b7..6542d8e 100644
--- a/java/com/google/gerrit/extensions/events/ChangeEvent.java
+++ b/java/com/google/gerrit/extensions/events/ChangeEvent.java
@@ -16,7 +16,7 @@
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
-import java.sql.Timestamp;
+import java.time.Instant;
/** Interface to be extended by Events with a Change. */
public interface ChangeEvent extends GerritEvent {
@@ -29,5 +29,5 @@
AccountInfo getWho();
- Timestamp getWhen();
+ Instant getWhen();
}
diff --git a/java/com/google/gerrit/extensions/registration/DynamicMap.java b/java/com/google/gerrit/extensions/registration/DynamicMap.java
index 48b1279..6fd2c03 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicMap.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicMap.java
@@ -24,8 +24,8 @@
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
-import java.util.SortedMap;
-import java.util.SortedSet;
+import java.util.NavigableMap;
+import java.util.NavigableSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
@@ -116,14 +116,14 @@
/**
* Get the names of all running plugins supplying this type.
*
- * @return sorted set of active plugins that supply at least one item.
+ * @return navigatable set of active plugins that supply at least one item.
*/
- public SortedSet<String> plugins() {
- SortedSet<String> r = new TreeSet<>();
+ public NavigableSet<String> plugins() {
+ NavigableSet<String> r = new TreeSet<>();
for (NamePair p : items.keySet()) {
r.add(p.pluginName);
}
- return Collections.unmodifiableSortedSet(r);
+ return Collections.unmodifiableNavigableSet(r);
}
/**
@@ -132,21 +132,21 @@
* @param pluginName name of the plugin.
* @return items exported by a plugin, keyed by the export name.
*/
- public SortedMap<String, Provider<T>> byPlugin(String pluginName) {
- SortedMap<String, Provider<T>> r = new TreeMap<>();
+ public NavigableMap<String, Provider<T>> byPlugin(String pluginName) {
+ NavigableMap<String, Provider<T>> r = new TreeMap<>();
for (Map.Entry<NamePair, Provider<T>> e : items.entrySet()) {
if (e.getKey().pluginName.equals(pluginName)) {
r.put(e.getKey().exportName, e.getValue());
}
}
- return Collections.unmodifiableSortedMap(r);
+ return Collections.unmodifiableNavigableMap(r);
}
/** Iterate through all entries in an undefined order. */
@Override
public Iterator<Extension<T>> iterator() {
final Iterator<Map.Entry<NamePair, Provider<T>>> i = items.entrySet().iterator();
- return new Iterator<Extension<T>>() {
+ return new Iterator<>() {
@Override
public boolean hasNext() {
return i.hasNext();
diff --git a/java/com/google/gerrit/extensions/registration/DynamicSet.java b/java/com/google/gerrit/extensions/registration/DynamicSet.java
index b2e871e..a0b2c6a 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -153,7 +153,7 @@
@Override
public Iterator<T> iterator() {
Iterator<Extension<T>> entryIterator = entries().iterator();
- return new Iterator<T>() {
+ return new Iterator<>() {
@Override
public boolean hasNext() {
return entryIterator.hasNext();
@@ -170,7 +170,7 @@
public Iterable<Extension<T>> entries() {
final Iterator<AtomicReference<Extension<T>>> itr = items.iterator();
return () ->
- new Iterator<Extension<T>>() {
+ new Iterator<>() {
private Extension<T> next;
@Override
diff --git a/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java b/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
index 832933b..d8999e3 100644
--- a/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
+++ b/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
@@ -14,13 +14,13 @@
package com.google.gerrit.extensions.registration;
+import com.google.common.collect.ImmutableList;
import com.google.inject.Binding;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Provider;
import com.google.inject.TypeLiteral;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
@@ -38,11 +38,12 @@
return new DynamicSet<>(find(injector, type));
}
- private static <T> List<AtomicReference<Extension<T>>> find(Injector src, TypeLiteral<T> type) {
+ private static <T> ImmutableList<AtomicReference<Extension<T>>> find(
+ Injector src, TypeLiteral<T> type) {
List<Binding<T>> bindings = src.findBindingsByType(type);
int cnt = bindings != null ? bindings.size() : 0;
if (cnt == 0) {
- return Collections.emptyList();
+ return ImmutableList.of();
}
List<AtomicReference<Extension<T>>> r = new ArrayList<>(cnt);
for (Binding<T> b : bindings) {
@@ -50,6 +51,6 @@
r.add(new AtomicReference<>(new Extension<>(PluginName.GERRIT, b.getProvider())));
}
}
- return r;
+ return ImmutableList.copyOf(r);
}
}
diff --git a/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java b/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
index fd31fcd..5b528cb 100644
--- a/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
+++ b/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
@@ -14,6 +14,7 @@
package com.google.gerrit.extensions.registration;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.inject.Binding;
import com.google.inject.Inject;
@@ -80,10 +81,10 @@
return Collections.unmodifiableMap(m);
}
- public static List<RegistrationHandle> attachItems(
+ public static ImmutableList<RegistrationHandle> attachItems(
Injector src, String pluginName, Map<TypeLiteral<?>, DynamicItem<?>> items) {
if (src == null || items == null || items.isEmpty()) {
- return Collections.emptyList();
+ return ImmutableList.of();
}
List<RegistrationHandle> handles = new ArrayList<>(4);
@@ -103,13 +104,13 @@
remove(handles);
throw e;
}
- return handles;
+ return ImmutableList.copyOf(handles);
}
- public static List<RegistrationHandle> attachSets(
+ public static ImmutableList<RegistrationHandle> attachSets(
Injector src, String pluginName, Map<TypeLiteral<?>, DynamicSet<?>> sets) {
if (src == null || sets == null || sets.isEmpty()) {
- return Collections.emptyList();
+ return ImmutableList.of();
}
List<RegistrationHandle> handles = new ArrayList<>(4);
@@ -131,13 +132,13 @@
remove(handles);
throw e;
}
- return handles;
+ return ImmutableList.copyOf(handles);
}
- public static List<RegistrationHandle> attachMaps(
+ public static ImmutableList<RegistrationHandle> attachMaps(
Injector src, String pluginName, Map<TypeLiteral<?>, DynamicMap<?>> maps) {
if (src == null || maps == null || maps.isEmpty()) {
- return Collections.emptyList();
+ return ImmutableList.of();
}
List<RegistrationHandle> handles = new ArrayList<>(4);
@@ -160,7 +161,7 @@
remove(handles);
throw e;
}
- return handles;
+ return ImmutableList.copyOf(handles);
}
public static LifecycleListener registerInParentInjectors() {
diff --git a/java/com/google/gerrit/extensions/validators/CommentValidationContext.java b/java/com/google/gerrit/extensions/validators/CommentValidationContext.java
index db08058..2c9d8f2 100644
--- a/java/com/google/gerrit/extensions/validators/CommentValidationContext.java
+++ b/java/com/google/gerrit/extensions/validators/CommentValidationContext.java
@@ -34,7 +34,10 @@
/** Returns the project the comment is being added to. */
public abstract String getProject();
- public static CommentValidationContext create(int changeId, String project) {
- return new AutoValue_CommentValidationContext(changeId, project);
+ /** Returns the ref name the comment is being added to. */
+ public abstract String getRefName();
+
+ public static CommentValidationContext create(int changeId, String project, String refName) {
+ return new AutoValue_CommentValidationContext(changeId, project, refName);
}
}
diff --git a/java/com/google/gerrit/gpg/PublicKeyChecker.java b/java/com/google/gerrit/gpg/PublicKeyChecker.java
index 27530e7..0a96212 100644
--- a/java/com/google/gerrit/gpg/PublicKeyChecker.java
+++ b/java/com/google/gerrit/gpg/PublicKeyChecker.java
@@ -32,9 +32,9 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
import java.io.IOException;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -62,7 +62,7 @@
private PublicKeyStore store;
private Map<Long, Fingerprint> trusted;
private int maxTrustDepth;
- private Date effectiveTime = new Date();
+ private Instant effectiveTime = Instant.now();
/**
* Enable web-of-trust checks.
@@ -111,12 +111,12 @@
* @param effectiveTime effective time.
* @return a reference to this object.
*/
- public PublicKeyChecker setEffectiveTime(Date effectiveTime) {
+ public PublicKeyChecker setEffectiveTime(Instant effectiveTime) {
this.effectiveTime = effectiveTime;
return this;
}
- protected Date getEffectiveTime() {
+ protected Instant getEffectiveTime() {
return effectiveTime;
}
@@ -183,13 +183,13 @@
return CheckResult.create(status, problems);
}
- private CheckResult checkBasic(PGPPublicKey key, Date now) {
+ private CheckResult checkBasic(PGPPublicKey key, Instant now) {
List<String> problems = new ArrayList<>(2);
gatherRevocationProblems(key, now, problems);
long validMs = key.getValidSeconds() * 1000;
if (validMs != 0) {
- long msSinceCreation = now.getTime() - key.getCreationTime().getTime();
+ long msSinceCreation = now.toEpochMilli() - getCreationTime(key).toEpochMilli();
if (msSinceCreation > validMs) {
problems.add("Key is expired");
}
@@ -197,7 +197,7 @@
return CheckResult.create(problems);
}
- private void gatherRevocationProblems(PGPPublicKey key, Date now, List<String> problems) {
+ private void gatherRevocationProblems(PGPPublicKey key, Instant now, List<String> problems) {
try {
List<PGPSignature> revocations = new ArrayList<>();
Map<Long, RevocationKey> revokers = new HashMap<>();
@@ -216,7 +216,7 @@
}
private static boolean isRevocationValid(
- PGPSignature revocation, RevocationReason reason, Date now) {
+ PGPSignature revocation, RevocationReason reason, Instant now) {
// RFC4880 states:
// "If a key has been revoked because of a compromise, all signatures
// created by that key are suspect. However, if it was merely superseded or
@@ -226,11 +226,14 @@
// consider the revocation reason and timestamp when checking whether a
// signature (data or certification) is valid.
return reason.getRevocationReason() == KEY_COMPROMISED
- || revocation.getCreationTime().before(now);
+ || PushCertificateChecker.getCreationTime(revocation).isBefore(now);
}
private PGPSignature scanRevocations(
- PGPPublicKey key, Date now, List<PGPSignature> revocations, Map<Long, RevocationKey> revokers)
+ PGPPublicKey key,
+ Instant now,
+ List<PGPSignature> revocations,
+ Map<Long, RevocationKey> revokers)
throws PGPException {
@SuppressWarnings("unchecked")
Iterator<PGPSignature> allSigs = key.getSignatures();
@@ -305,7 +308,7 @@
if (rk.getAlgorithm() != revoker.getAlgorithm()) {
continue;
}
- if (!checkBasic(rk, revocation.getCreationTime()).isOk()) {
+ if (!checkBasic(rk, PushCertificateChecker.getCreationTime(revocation)).isOk()) {
// Revoker's key was expired or revoked at time of revocation, so the
// revocation is invalid.
continue;
@@ -469,4 +472,9 @@
}
return null;
}
+
+ @SuppressWarnings("JdkObsolete")
+ private static Instant getCreationTime(PGPPublicKey key) {
+ return key.getCreationTime().toInstant();
+ }
}
diff --git a/java/com/google/gerrit/gpg/PushCertificateChecker.java b/java/com/google/gerrit/gpg/PushCertificateChecker.java
index 36a4af7..17ca5a4 100644
--- a/java/com/google/gerrit/gpg/PushCertificateChecker.java
+++ b/java/com/google/gerrit/gpg/PushCertificateChecker.java
@@ -25,6 +25,7 @@
import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import org.bouncycastle.bcpg.ArmoredInputStream;
@@ -106,7 +107,7 @@
}
} catch (PGPException | IOException e) {
String msg = "Internal error checking push certificate";
- logger.atSevere().withCause(e).log(msg);
+ logger.atSevere().withCause(e).log("%s", msg);
results.add(CheckResult.bad(msg));
}
@@ -205,7 +206,7 @@
null, CheckResult.bad("Signature by " + keyIdToString(sig.getKeyID()) + " is not valid"));
}
CheckResult result =
- publicKeyChecker.setStore(store).setEffectiveTime(sig.getCreationTime()).check(signer);
+ publicKeyChecker.setStore(store).setEffectiveTime(getCreationTime(sig)).check(signer);
if (!result.getProblems().isEmpty()) {
StringBuilder err =
new StringBuilder("Invalid public key ")
@@ -216,4 +217,9 @@
}
return new Result(signer, result);
}
+
+ @SuppressWarnings("JdkObsolete")
+ public static Instant getCreationTime(PGPSignature signature) {
+ return signature.getCreationTime().toInstant();
+ }
}
diff --git a/java/com/google/gerrit/gpg/server/DeleteGpgKey.java b/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
index e0c921d..bcc8631 100644
--- a/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
+++ b/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
@@ -95,7 +95,7 @@
CommitBuilder cb = new CommitBuilder();
PersonIdent committer = serverIdent.get();
- cb.setAuthor(rsrc.getUser().newCommitterIdent(committer.getWhen(), committer.getTimeZone()));
+ cb.setAuthor(rsrc.getUser().newCommitterIdent(committer));
cb.setCommitter(committer);
cb.setMessage("Delete public key " + keyIdToString(key.getKeyID()));
diff --git a/java/com/google/gerrit/gpg/server/GpgKey.java b/java/com/google/gerrit/gpg/server/GpgKey.java
index aa6b6f4..fbe97ad 100644
--- a/java/com/google/gerrit/gpg/server/GpgKey.java
+++ b/java/com/google/gerrit/gpg/server/GpgKey.java
@@ -21,8 +21,7 @@
import org.bouncycastle.openpgp.PGPPublicKeyRing;
public class GpgKey extends AccountResource {
- public static final TypeLiteral<RestView<GpgKey>> GPG_KEY_KIND =
- new TypeLiteral<RestView<GpgKey>>() {};
+ public static final TypeLiteral<RestView<GpgKey>> GPG_KEY_KIND = new TypeLiteral<>() {};
private final PGPPublicKeyRing keyRing;
diff --git a/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index d46b344..3341806 100644
--- a/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -164,7 +164,7 @@
}
}
- private Map<ExternalId, Fingerprint> readKeysToRemove(
+ private ImmutableMap<ExternalId, Fingerprint> readKeysToRemove(
GpgKeysInput input, Collection<ExternalId> existingExtIds) {
if (input.delete == null || input.delete.isEmpty()) {
return ImmutableMap.of();
@@ -179,10 +179,11 @@
// Skip removal.
}
}
- return fingerprints;
+ return ImmutableMap.copyOf(fingerprints);
}
- private List<PGPPublicKeyRing> readKeysToAdd(GpgKeysInput input, Collection<Fingerprint> toRemove)
+ private ImmutableList<PGPPublicKeyRing> readKeysToAdd(
+ GpgKeysInput input, Collection<Fingerprint> toRemove)
throws BadRequestException, IOException {
if (input.add == null || input.add.isEmpty()) {
return ImmutableList.of();
@@ -206,7 +207,7 @@
throw new BadRequestException("Failed to parse GPG keys", e);
}
}
- return keyRings;
+ return ImmutableList.copyOf(keyRings);
}
private void storeKeys(
@@ -249,7 +250,7 @@
}
CommitBuilder cb = new CommitBuilder();
PersonIdent committer = serverIdent.get();
- cb.setAuthor(user.newCommitterIdent(committer.getWhen(), committer.getTimeZone()));
+ cb.setAuthor(user.newCommitterIdent(committer));
cb.setCommitter(committer);
RefUpdate.Result saveResult = store.save(cb);
diff --git a/java/com/google/gerrit/httpd/CanonicalWebUrl.java b/java/com/google/gerrit/httpd/CanonicalWebUrl.java
index 437ddf3..3b04884 100644
--- a/java/com/google/gerrit/httpd/CanonicalWebUrl.java
+++ b/java/com/google/gerrit/httpd/CanonicalWebUrl.java
@@ -37,6 +37,7 @@
return url != null ? url : computeFromRequest(req);
}
+ @SuppressWarnings("JdkObsolete")
static String computeFromRequest(HttpServletRequest req) {
StringBuffer url = req.getRequestURL();
try {
diff --git a/java/com/google/gerrit/httpd/CookieBase64.java b/java/com/google/gerrit/httpd/CookieBase64.java
index 52cfde7..376ae1d 100644
--- a/java/com/google/gerrit/httpd/CookieBase64.java
+++ b/java/com/google/gerrit/httpd/CookieBase64.java
@@ -75,7 +75,7 @@
out.append(enc[(inBuff >>> 18)]);
out.append(enc[(inBuff >>> 12) & 0x3f]);
out.append(enc[(inBuff >>> 6) & 0x3f]);
- out.append(enc[(inBuff) & 0x3f]);
+ out.append(enc[inBuff & 0x3f]);
break;
case 2:
diff --git a/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 56afe40..7ed79c4 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -527,7 +527,7 @@
throws ServiceNotAuthorizedException {
final ProjectState state = (ProjectState) req.getAttribute(ATT_STATE);
- if (!(userProvider.get().isIdentifiedUser())) {
+ if (!userProvider.get().isIdentifiedUser()) {
// Anonymous users are not permitted to push.
throw new ServiceNotAuthorizedException();
}
@@ -634,7 +634,7 @@
return;
}
- if (!(userProvider.get().isIdentifiedUser())) {
+ if (!userProvider.get().isIdentifiedUser()) {
chain.doFilter(request, responseWrapper);
return;
}
diff --git a/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java b/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
index a421139..b0c7615 100644
--- a/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
+++ b/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
@@ -186,7 +186,7 @@
if (passwordVerifier.checkPassword(who.externalIds(), username, password)) {
return succeedAuthentication(who, null);
}
- logger.atWarning().withCause(e).log(authenticationFailedMsg(username, req));
+ logger.atWarning().withCause(e).log("%s", authenticationFailedMsg(username, req));
rsp.sendError(SC_UNAUTHORIZED);
return false;
} catch (AuthenticationFailedException e) {
@@ -200,7 +200,7 @@
rsp.sendError(SC_SERVICE_UNAVAILABLE);
return false;
} catch (AccountException e) {
- logger.atWarning().withCause(e).log(authenticationFailedMsg(username, req));
+ logger.atWarning().withCause(e).log("%s", authenticationFailedMsg(username, req));
rsp.sendError(SC_UNAUTHORIZED);
return false;
}
@@ -214,8 +214,8 @@
private boolean failAuthentication(Response rsp, String username, HttpServletRequest req)
throws IOException {
logger.atWarning().log(
- authenticationFailedMsg(username, req)
- + ": password does not match the one stored in Gerrit");
+ "%s: password does not match the one stored in Gerrit",
+ authenticationFailedMsg(username, req));
rsp.sendError(SC_UNAUTHORIZED);
return false;
}
diff --git a/java/com/google/gerrit/httpd/ProjectOAuthFilter.java b/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
index fa53053..de6ae50 100644
--- a/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
+++ b/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
@@ -158,8 +158,8 @@
accountCache.getByUsername(authInfo.username).filter(a -> a.account().isActive());
if (!who.isPresent()) {
logger.atWarning().log(
- authenticationFailedMsg(authInfo.username, req)
- + ": account inactive or not provisioned in Gerrit");
+ "%s: account inactive or not provisioned in Gerrit",
+ authenticationFailedMsg(authInfo.username, req));
rsp.sendError(SC_UNAUTHORIZED);
return false;
}
@@ -180,7 +180,7 @@
ws.setAccessPathOk(AccessPath.REST_API, true);
return true;
} catch (AccountException e) {
- logger.atWarning().withCause(e).log(authenticationFailedMsg(authInfo.username, req));
+ logger.atWarning().withCause(e).log("%s", authenticationFailedMsg(authInfo.username, req));
rsp.sendError(SC_UNAUTHORIZED);
return false;
}
diff --git a/java/com/google/gerrit/httpd/RequireSslFilter.java b/java/com/google/gerrit/httpd/RequireSslFilter.java
index a4a87e2..ca3c3d8 100644
--- a/java/com/google/gerrit/httpd/RequireSslFilter.java
+++ b/java/com/google/gerrit/httpd/RequireSslFilter.java
@@ -78,10 +78,7 @@
//
final String url;
if (isLocalHost(req)) {
- final StringBuffer b = req.getRequestURL();
- b.replace(0, b.indexOf(":"), "https");
- url = b.toString();
-
+ url = getLocalHostUrl(req);
} else {
url = urlProvider.get() + req.getServletPath();
}
@@ -90,6 +87,13 @@
}
}
+ @SuppressWarnings("JdkObsolete")
+ private static String getLocalHostUrl(HttpServletRequest req) {
+ StringBuffer b = req.getRequestURL();
+ b.replace(0, b.indexOf(":"), "https");
+ return b.toString();
+ }
+
private static boolean isSecure(HttpServletRequest req) {
return "https".equals(req.getScheme()) || req.isSecure();
}
diff --git a/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java b/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
index 820c7a2..59a7379 100644
--- a/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
+++ b/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
@@ -14,7 +14,6 @@
package com.google.gerrit.httpd.auth.container;
-import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.server.account.AccountException;
@@ -36,8 +35,6 @@
@Singleton
class HttpsClientSslCertAuthFilter implements Filter {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
private static final Pattern REGEX_USERID = Pattern.compile("CN=([^,]*)");
private final DynamicItem<WebSession> webSession;
@@ -79,9 +76,7 @@
try {
arsp = accountManager.authenticate(areq);
} catch (AccountException e) {
- String err = "Unable to authenticate user \"" + userName + "\"";
- logger.atSevere().withCause(e).log(err);
- throw new ServletException(err, e);
+ throw new ServletException("Unable to authenticate user \"" + userName + "\"", e);
}
webSession.get().login(arsp, true);
chain.doFilter(req, rsp);
diff --git a/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java b/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
index a3f8fbda..297505a 100644
--- a/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
+++ b/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
@@ -190,7 +190,7 @@
} else if (claimedId.isPresent() && !actualId.isPresent()) {
// Claimed account already exists: link to it.
//
- logger.atInfo().log("OAuth2: linking claimed identity to %s", claimedId.get().toString());
+ logger.atInfo().log("OAuth2: linking claimed identity to %s", claimedId.get());
try {
accountManager.link(claimedId.get(), req);
} catch (ConfigInvalidException e) {
diff --git a/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java b/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
index 2642a543..935762f 100644
--- a/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
+++ b/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
@@ -31,9 +31,9 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Map;
+import java.util.NavigableMap;
+import java.util.NavigableSet;
import java.util.Set;
-import java.util.SortedMap;
-import java.util.SortedSet;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
@@ -175,12 +175,12 @@
}
private void pickSSOServiceProvider() throws ServletException {
- SortedSet<String> plugins = oauthServiceProviders.plugins();
+ NavigableSet<String> plugins = oauthServiceProviders.plugins();
if (plugins.isEmpty()) {
throw new ServletException("OAuth service provider wasn't installed");
}
if (plugins.size() == 1) {
- SortedMap<String, Provider<OAuthServiceProvider>> services =
+ NavigableMap<String, Provider<OAuthServiceProvider>> services =
oauthServiceProviders.byPlugin(Iterables.getOnlyElement(plugins));
if (services.size() == 1) {
ssoProvider = Iterables.getOnlyElement(services.values()).get();
diff --git a/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java b/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java
index 2e8585d..3d9c819 100644
--- a/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java
+++ b/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java
@@ -21,8 +21,8 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.SortedMap;
-import java.util.SortedSet;
+import java.util.NavigableMap;
+import java.util.NavigableSet;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
@@ -79,9 +79,9 @@
}
private void pickSSOServiceProvider() {
- SortedSet<String> plugins = oauthServiceProviders.plugins();
+ NavigableSet<String> plugins = oauthServiceProviders.plugins();
if (plugins.size() == 1) {
- SortedMap<String, Provider<OAuthServiceProvider>> services =
+ NavigableMap<String, Provider<OAuthServiceProvider>> services =
oauthServiceProviders.byPlugin(Iterables.getOnlyElement(plugins));
if (services.size() == 1) {
ssoProvider = Iterables.getOnlyElement(services.values()).get();
diff --git a/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
index 897d96f..ba4d5f0 100644
--- a/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
+++ b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
@@ -73,7 +73,7 @@
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.Enumeration;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -133,7 +133,7 @@
this.deniedActions = new HashSet<>();
final String url = gitwebConfig.getUrl();
- if ((url != null) && (!url.equals("gitweb"))) {
+ if (url != null && !url.equals("gitweb")) {
URI uri = null;
try {
uri = new URI(url);
@@ -568,9 +568,7 @@
env.set("SERVER_PROTOCOL", req.getProtocol());
env.set("SERVER_SOFTWARE", getServletContext().getServerInfo());
- final Enumeration<String> hdrs = enumerateHeaderNames(req);
- while (hdrs.hasMoreElements()) {
- final String name = hdrs.nextElement();
+ for (String name : getHeaderNames(req)) {
final String value = req.getHeader(name);
env.set("HTTP_" + name.toUpperCase().replace('-', '_'), value);
}
@@ -677,7 +675,7 @@
.collect(Collectors.joining("\n"))
.trim();
if (!err.isEmpty()) {
- logger.atSevere().log(err);
+ logger.atSevere().log("%s", err);
}
} catch (IOException e) {
logger.atSevere().withCause(e).log("Unexpected error copying stderr from CGI");
@@ -687,10 +685,6 @@
.start();
}
- private static Enumeration<String> enumerateHeaderNames(HttpServletRequest req) {
- return req.getHeaderNames();
- }
-
private void readCgiHeaders(HttpServletResponse res, InputStream in) throws IOException {
String line;
while (!(line = readLine(in)).isEmpty()) {
@@ -731,6 +725,11 @@
return buf.toString().trim();
}
+ @SuppressWarnings("JdkObsolete")
+ private static Iterable<String> getHeaderNames(HttpServletRequest req) {
+ return Collections.list(req.getHeaderNames());
+ }
+
/** private utility class that manages the Environment passed to exec. */
private static class EnvList {
private Map<String, String> envMap;
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 68cf1b2..1535c87 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -345,7 +345,7 @@
});
modules.add(new DefaultUrlFormatterModule());
- SshSessionFactoryInitializer.init(config);
+ SshSessionFactoryInitializer.init();
modules.add(SshKeyCacheImpl.module());
modules.add(
new AbstractModule() {
diff --git a/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index ef37fc5..e3a401a 100644
--- a/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -68,7 +68,6 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -447,8 +446,7 @@
return false;
};
- List<PluginEntry> entries =
- Collections.list(scanner.entries()).stream().filter(filter).collect(toList());
+ List<PluginEntry> entries = scanner.entries().filter(filter).collect(toList());
for (PluginEntry entry : entries) {
String name = entry.getName().substring(prefix.length());
if (name.startsWith("cmd-")) {
@@ -520,7 +518,7 @@
macros.put("URL", url);
Matcher m = Pattern.compile("(\\\\)?@([A-Z_]+)@").matcher(md);
- StringBuffer sb = new StringBuffer();
+ StringBuilder sb = new StringBuilder();
while (m.find()) {
String key = m.group(2);
String val = macros.get(key);
diff --git a/java/com/google/gerrit/httpd/plugins/PluginServletContext.java b/java/com/google/gerrit/httpd/plugins/PluginServletContext.java
index 5a8fa31..5e875d7 100644
--- a/java/com/google/gerrit/httpd/plugins/PluginServletContext.java
+++ b/java/com/google/gerrit/httpd/plugins/PluginServletContext.java
@@ -61,11 +61,11 @@
try {
handler = API.class.getDeclaredMethod(method.getName(), method.getParameterTypes());
} catch (NoSuchMethodException e) {
- String msg =
- String.format(
- "%s does not implement %s", PluginServletContext.class, method.toGenericString());
- logger.atSevere().withCause(e).log(msg);
- throw new NoSuchMethodError(msg);
+ throw new NoSuchMethodError(
+ String.format(
+ "%s does not implement %s",
+ PluginServletContext.class, method.toGenericString()))
+ .initCause(e);
}
return handler.invoke(this, args);
}
diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
index 445a73a..ce22ae8 100644
--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
@@ -97,7 +97,8 @@
IndexPreloadingUtil.computeChangeRequestsPath(requestedPath, page).get());
break;
case DIFF:
- data.put("defaultDiffDetailHex", ListOption.toHex(IndexPreloadingUtil.DIFF_OPTIONS));
+ data.put(
+ "defaultChangeDetailHex", ListOption.toHex(IndexPreloadingUtil.CHANGE_DETAIL_OPTIONS));
data.put(
"changeRequestsPath",
IndexPreloadingUtil.computeChangeRequestsPath(requestedPath, page).get());
diff --git a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
index 3bdcb1a..8395d12 100644
--- a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
@@ -101,13 +101,8 @@
ListChangesOption.MESSAGES,
ListChangesOption.SUBMITTABLE,
ListChangesOption.WEB_LINKS,
- ListChangesOption.SKIP_DIFFSTAT);
-
- public static final ImmutableSet<ListChangesOption> DIFF_OPTIONS =
- ImmutableSet.of(
- ListChangesOption.ALL_COMMITS,
- ListChangesOption.ALL_REVISIONS,
- ListChangesOption.SKIP_DIFFSTAT);
+ ListChangesOption.SKIP_DIFFSTAT,
+ ListChangesOption.SUBMIT_REQUIREMENTS);
public static String getPath(@Nullable String requestedURL) throws URISyntaxException {
if (requestedURL == null) {
diff --git a/java/com/google/gerrit/httpd/raw/IndexServlet.java b/java/com/google/gerrit/httpd/raw/IndexServlet.java
index 3f2c202..fcb821e 100644
--- a/java/com/google/gerrit/httpd/raw/IndexServlet.java
+++ b/java/com/google/gerrit/httpd/raw/IndexServlet.java
@@ -38,6 +38,8 @@
public class IndexServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
+ private static final String POLY_GERRIT_INDEX_HTML_SOY =
+ "com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy";
@Nullable private final String canonicalUrl;
@Nullable private final String cdnPath;
@@ -60,7 +62,7 @@
this.experimentFeatures = experimentFeatures;
this.soySauce =
SoyFileSet.builder()
- .add(Resources.getResource("com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy"))
+ .add(Resources.getResource(POLY_GERRIT_INDEX_HTML_SOY), POLY_GERRIT_INDEX_HTML_SOY)
.build()
.compileTemplates();
this.urlOrdainer =
@@ -74,7 +76,6 @@
SoySauce.Renderer renderer;
try {
Map<String, String[]> parameterMap = req.getParameterMap();
- String requestUrl = req.getRequestURL() == null ? null : req.getRequestURL().toString();
// TODO(hiesel): Remove URL ordainer as parameter once Soy is consistent
ImmutableMap<String, Object> templateData =
IndexHtmlUtil.templateData(
@@ -85,7 +86,7 @@
faviconPath,
parameterMap,
urlOrdainer,
- requestUrl);
+ getRequestUrl(req));
renderer = soySauce.renderTemplate("com.google.gerrit.httpd.raw.Index").setData(templateData);
} catch (URISyntaxException | RestApiException e) {
throw new IOException(e);
@@ -98,4 +99,13 @@
w.write(renderer.renderHtml().get().toString().getBytes(UTF_8));
}
}
+
+ @SuppressWarnings("JdkObsolete")
+ @Nullable
+ private static String getRequestUrl(HttpServletRequest req) {
+ if (req.getRequestURL() == null) {
+ return null;
+ }
+ return req.getRequestURL().toString();
+ }
}
diff --git a/java/com/google/gerrit/httpd/raw/StaticModule.java b/java/com/google/gerrit/httpd/raw/StaticModule.java
index aa32169..460ad60 100644
--- a/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -79,6 +79,7 @@
"/dashboard/*",
"/groups/self",
"/settings/*",
+ "/topic/*",
"/Documentation/q/*");
/**
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 369ea29..b7dd2f4 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -825,7 +825,7 @@
if (isRead(request)) {
logger.atWarning().log(
"request %s performed a ref update %s although the request is a READ request",
- request.getRequestURL().toString(), refUpdateFormat);
+ request.getRequestURL(), refUpdateFormat);
}
response.addHeader(X_GERRIT_UPDATED_REF, refUpdateFormat);
}
@@ -1714,7 +1714,7 @@
private static List<IdString> splitPath(HttpServletRequest req) {
String path = RequestUtil.getEncodedPathInfo(req);
if (Strings.isNullOrEmpty(path)) {
- return Collections.emptyList();
+ return new ArrayList<>();
}
List<IdString> out = new ArrayList<>();
for (String p : Splitter.on('/').split(path)) {
diff --git a/java/com/google/gerrit/index/query/AndPredicate.java b/java/com/google/gerrit/index/query/AndPredicate.java
index ae13fb3..fd8ca96 100644
--- a/java/com/google/gerrit/index/query/AndPredicate.java
+++ b/java/com/google/gerrit/index/query/AndPredicate.java
@@ -105,6 +105,8 @@
return getChild(0).hashCode() * 31 + getChild(1).hashCode();
}
+ // Suppress the EqualsGetClass warning as this is legacy code.
+ @SuppressWarnings("EqualsGetClass")
@Override
public boolean equals(Object other) {
if (other == null) {
diff --git a/java/com/google/gerrit/index/query/IndexPredicate.java b/java/com/google/gerrit/index/query/IndexPredicate.java
index 18d7fbc..b65fb96 100644
--- a/java/com/google/gerrit/index/query/IndexPredicate.java
+++ b/java/com/google/gerrit/index/query/IndexPredicate.java
@@ -35,7 +35,7 @@
* complexity was reduced to the bare minimum at the cost of small discrepancies to the Unicode
* spec.
*/
- private static final Splitter FULL_TEXT_SPLITTER = Splitter.on(CharMatcher.anyOf(" ,.-:\\/_\n"));
+ private static final Splitter FULL_TEXT_SPLITTER = Splitter.on(CharMatcher.anyOf(" ,.-:\\/_=\n"));
private final FieldDef<I, ?> def;
diff --git a/java/com/google/gerrit/index/query/IndexedQuery.java b/java/com/google/gerrit/index/query/IndexedQuery.java
index d9e33ea..ffd442b 100644
--- a/java/com/google/gerrit/index/query/IndexedQuery.java
+++ b/java/com/google/gerrit/index/query/IndexedQuery.java
@@ -112,6 +112,8 @@
return pred.hashCode();
}
+ // Suppress the EqualsGetClass warning as this is legacy code.
+ @SuppressWarnings("EqualsGetClass")
@Override
public boolean equals(Object other) {
if (other == null || getClass() != other.getClass()) {
diff --git a/java/com/google/gerrit/index/query/IntPredicate.java b/java/com/google/gerrit/index/query/IntPredicate.java
index 16e59e7..a98e0b1 100644
--- a/java/com/google/gerrit/index/query/IntPredicate.java
+++ b/java/com/google/gerrit/index/query/IntPredicate.java
@@ -37,6 +37,8 @@
return getOperator().hashCode() * 31 + intValue;
}
+ // Suppress the EqualsGetClass warning as this is legacy code.
+ @SuppressWarnings("EqualsGetClass")
@Override
public boolean equals(Object other) {
if (other == null) {
diff --git a/java/com/google/gerrit/index/query/NotPredicate.java b/java/com/google/gerrit/index/query/NotPredicate.java
index 14cb740..fa8e01b 100644
--- a/java/com/google/gerrit/index/query/NotPredicate.java
+++ b/java/com/google/gerrit/index/query/NotPredicate.java
@@ -21,10 +21,10 @@
import java.util.List;
/** Negates the result of another predicate. */
-public class NotPredicate<T> extends Predicate<T> implements Matchable<T> {
+public final class NotPredicate<T> extends Predicate<T> implements Matchable<T> {
private final Predicate<T> that;
- protected NotPredicate(Predicate<T> that) {
+ NotPredicate(Predicate<T> that) {
if (that instanceof NotPredicate) {
throw new IllegalArgumentException("Double negation unsupported");
}
@@ -87,7 +87,7 @@
if (other == null) {
return false;
}
- return getClass() == other.getClass()
+ return other instanceof NotPredicate
&& getChildren().equals(((Predicate<?>) other).getChildren());
}
diff --git a/java/com/google/gerrit/index/query/OperatorPredicate.java b/java/com/google/gerrit/index/query/OperatorPredicate.java
index 368ee24..ea7717f 100644
--- a/java/com/google/gerrit/index/query/OperatorPredicate.java
+++ b/java/com/google/gerrit/index/query/OperatorPredicate.java
@@ -47,6 +47,8 @@
return getOperator().hashCode() * 31 + getValue().hashCode();
}
+ // Suppress the EqualsGetClass warning as this is legacy code.
+ @SuppressWarnings("EqualsGetClass")
@Override
public boolean equals(Object other) {
if (other == null) {
diff --git a/java/com/google/gerrit/index/query/OrPredicate.java b/java/com/google/gerrit/index/query/OrPredicate.java
index 9bc3769..1c31af3 100644
--- a/java/com/google/gerrit/index/query/OrPredicate.java
+++ b/java/com/google/gerrit/index/query/OrPredicate.java
@@ -105,6 +105,8 @@
return getChild(0).hashCode() * 31 + getChild(1).hashCode();
}
+ // Suppress the EqualsGetClass warning as this is legacy code.
+ @SuppressWarnings("EqualsGetClass")
@Override
public boolean equals(Object other) {
if (other == null) {
diff --git a/java/com/google/gerrit/index/query/Predicate.java b/java/com/google/gerrit/index/query/Predicate.java
index 9dc7689..e251b00 100644
--- a/java/com/google/gerrit/index/query/Predicate.java
+++ b/java/com/google/gerrit/index/query/Predicate.java
@@ -18,10 +18,10 @@
import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.Iterables;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
@@ -152,7 +152,7 @@
/** Returns a list of this predicate and all its descendants. */
public List<Predicate<T>> getFlattenedPredicateList() {
List<Predicate<T>> result = new ArrayList<>();
- Queue<Predicate<T>> queue = new LinkedList<>();
+ Queue<Predicate<T>> queue = new ArrayDeque<>();
queue.add(this);
while (!queue.isEmpty()) {
Predicate<T> current = queue.poll();
diff --git a/java/com/google/gerrit/index/query/QueryProcessor.java b/java/com/google/gerrit/index/query/QueryProcessor.java
index ea23d91..0d5e7b3 100644
--- a/java/com/google/gerrit/index/query/QueryProcessor.java
+++ b/java/com/google/gerrit/index/query/QueryProcessor.java
@@ -245,7 +245,7 @@
// max for this user. The only way to see if there are more entities is to
// ask for one more result from the query.
QueryOptions opts = createOptions(indexConfig, start, limit + 1, getRequestedFields());
- logger.atFine().log("Query options: " + opts);
+ logger.atFine().log("Query options: %s", opts);
Predicate<T> pred = rewriter.rewrite(q, opts);
if (enforceVisibility) {
pred = enforceVisibility(pred);
diff --git a/java/com/google/gerrit/index/query/TimestampRangePredicate.java b/java/com/google/gerrit/index/query/TimestampRangePredicate.java
index 42f8aa8..29d6f22 100644
--- a/java/com/google/gerrit/index/query/TimestampRangePredicate.java
+++ b/java/com/google/gerrit/index/query/TimestampRangePredicate.java
@@ -17,13 +17,13 @@
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.json.JavaSqlTimestampHelper;
import java.sql.Timestamp;
-import java.util.Date;
+import java.time.Instant;
// TODO: Migrate this to IntegerRangePredicate
public abstract class TimestampRangePredicate<I> extends IndexPredicate<I> {
- protected static Timestamp parse(String value) throws QueryParseException {
+ protected static Instant parse(String value) throws QueryParseException {
try {
- return JavaSqlTimestampHelper.parseTimestamp(value);
+ return JavaSqlTimestampHelper.parseTimestamp(value).toInstant();
} catch (IllegalArgumentException e) {
// parseTimestamp's errors are specific and helpful, so preserve them.
throw new QueryParseException(e.getMessage(), e);
@@ -38,7 +38,7 @@
return (Timestamp) this.getField().get(object);
}
- public abstract Date getMinTimestamp();
+ public abstract Instant getMinTimestamp();
- public abstract Date getMaxTimestamp();
+ public abstract Instant getMaxTimestamp();
}
diff --git a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
index 6ece45a..36e9e52 100644
--- a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
+++ b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
@@ -47,7 +47,7 @@
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@@ -121,7 +121,7 @@
.limit(opts.limit())
.collect(toImmutableList());
}
- return new DataSource<V>() {
+ return new DataSource<>() {
@Override
public int getCardinality() {
return results.size();
@@ -205,7 +205,7 @@
Comparator<ChangeData> lastUpdated =
Comparator.comparing(cd -> cd.change().getLastUpdatedOn());
Comparator<ChangeData> merged =
- Comparator.comparing(cd -> cd.getMergedOn().orElse(new Timestamp(0)));
+ Comparator.comparing(cd -> cd.getMergedOn().orElse(Instant.EPOCH));
Comparator<ChangeData> id = Comparator.comparing(cd -> cd.getId().get());
return lastUpdated.thenComparing(merged).thenComparing(id).reversed();
}
diff --git a/java/com/google/gerrit/json/BUILD b/java/com/google/gerrit/json/BUILD
index d9cec45..3c6bec6 100644
--- a/java/com/google/gerrit/json/BUILD
+++ b/java/com/google/gerrit/json/BUILD
@@ -6,5 +6,6 @@
visibility = ["//visibility:public"],
deps = [
"//lib:gson",
+ "//lib:guava",
],
)
diff --git a/java/com/google/gerrit/json/JavaSqlTimestampHelper.java b/java/com/google/gerrit/json/JavaSqlTimestampHelper.java
index b59cbd0d..35429f1 100644
--- a/java/com/google/gerrit/json/JavaSqlTimestampHelper.java
+++ b/java/com/google/gerrit/json/JavaSqlTimestampHelper.java
@@ -14,11 +14,19 @@
package com.google.gerrit.json;
+import com.google.common.base.Splitter;
import java.sql.Timestamp;
-import java.util.Date;
+import java.util.Calendar;
+import java.util.List;
+import java.util.TimeZone;
/** Utility to parse Timestamp from a string. */
public class JavaSqlTimestampHelper {
+
+ private static final Splitter TIMESTAMP_SPLITTER = Splitter.on(" ");
+ private static final Splitter DATE_SPLITTER = Splitter.on("-");
+ private static final Splitter TIME_SPLITTER = Splitter.on(":");
+
/**
* Parse a string into a timestamp.
*
@@ -31,22 +39,22 @@
* @return resulting timestamp.
*/
public static Timestamp parseTimestamp(String s) {
- String[] components = s.split(" ");
- if (components.length < 1 || components.length > 3) {
+ List<String> components = TIMESTAMP_SPLITTER.splitToList(s);
+ if (components.size() < 1 || components.size() > 3) {
throw new IllegalArgumentException("Expected date and optional time: " + s);
}
- String date = components[0];
- String time = components.length >= 2 ? components[1] : null;
- int off = components.length == 3 ? parseTimeZone(components[2]) : 0;
- String[] dSplit = date.split("-");
- if (dSplit.length != 3) {
+ String date = components.get(0);
+ String time = components.size() >= 2 ? components.get(1) : null;
+ int off = components.size() == 3 ? parseTimeZone(components.get(2)) : 0;
+ List<String> dSplit = DATE_SPLITTER.splitToList(date);
+ if (dSplit.size() != 3) {
throw new IllegalArgumentException("Invalid date format: " + date);
}
int yy, mm, dd;
try {
- yy = Integer.parseInt(dSplit[0]) - 1900;
- mm = Integer.parseInt(dSplit[1]) - 1;
- dd = Integer.parseInt(dSplit[2]);
+ yy = Integer.parseInt(dSplit.get(0));
+ mm = Integer.parseInt(dSplit.get(1)) - 1;
+ dd = Integer.parseInt(dSplit.get(2));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid date format: " + date, e);
}
@@ -64,13 +72,13 @@
t = time;
f = 0;
}
- String[] tSplit = t.split(":");
- if (tSplit.length != 3) {
+ List<String> tSplit = TIME_SPLITTER.splitToList(t);
+ if (tSplit.size() != 3) {
throw new IllegalArgumentException("Invalid time format: " + time);
}
- hh = Integer.parseInt(tSplit[0]);
- mi = Integer.parseInt(tSplit[1]);
- ss = Integer.parseInt(tSplit[2]);
+ hh = Integer.parseInt(tSplit.get(0));
+ mi = Integer.parseInt(tSplit.get(1));
+ ss = Integer.parseInt(tSplit.get(2));
ns = (int) Math.round(f * 1e9);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid time format: " + time, e);
@@ -81,8 +89,9 @@
ss = 0;
ns = 0;
}
- @SuppressWarnings("deprecation")
- Timestamp result = new Timestamp(Date.UTC(yy, mm, dd, hh, mi, ss) - off);
+ Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+ calendar.set(yy, mm, dd, hh, mi, ss);
+ Timestamp result = new Timestamp(calendar.toInstant().toEpochMilli() - off);
result.setNanos(ns);
return result;
}
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
index f6c395e..ceec55c 100644
--- a/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -41,12 +41,12 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.Enumeration;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.NavigableMap;
import java.util.Properties;
-import java.util.SortedMap;
import java.util.TreeMap;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
@@ -266,11 +266,11 @@
throw e;
}
- final SortedMap<String, URL> jars = new TreeMap<>();
+ final NavigableMap<String, URL> jars = new TreeMap<>();
try (ZipFile zf = new ZipFile(path)) {
- final Enumeration<? extends ZipEntry> e = zf.entries();
- while (e.hasMoreElements()) {
- final ZipEntry ze = e.nextElement();
+ Iterator<? extends ZipEntry> zipEntryIt = zf.stream().iterator();
+ while (zipEntryIt.hasNext()) {
+ final ZipEntry ze = zipEntryIt.next();
if (ze.isDirectory()) {
continue;
}
@@ -310,7 +310,7 @@
return URLClassLoader.newInstance(jars.values().toArray(new URL[jars.size()]), parent);
}
- private static void extractJar(ZipFile zf, ZipEntry ze, SortedMap<String, URL> jars)
+ private static void extractJar(ZipFile zf, ZipEntry ze, NavigableMap<String, URL> jars)
throws IOException {
File tmp = createTempFile(safeName(ze), ".jar");
try (OutputStream out = Files.newOutputStream(tmp.toPath());
@@ -326,8 +326,8 @@
jars.put(name.substring(name.lastIndexOf('/')), tmp.toURI().toURL());
}
- private static void move(SortedMap<String, URL> jars, String prefix, List<URL> extapi) {
- SortedMap<String, URL> matches = jars.tailMap(prefix);
+ private static void move(NavigableMap<String, URL> jars, String prefix, List<URL> extapi) {
+ NavigableMap<String, URL> matches = jars.tailMap(prefix, /* inclusive= */ true);
if (!matches.isEmpty()) {
String first = matches.firstKey();
if (first.startsWith(prefix)) {
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index 1e6ccac..988d6fb 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -109,6 +109,7 @@
private final AutoFlush autoFlush;
private ScheduledExecutorService autoCommitExecutor;
+ @SuppressWarnings("ThreadPriorityCheck")
AbstractLuceneIndex(
Schema<V> schema,
SitePaths sitePaths,
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index e7d47d0..9ea9d2e 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -384,7 +384,7 @@
}
ImmutableList<FieldBundle> fieldBundles =
documents.stream().map(rawDocumentMapper).collect(toImmutableList());
- return new ResultSet<FieldBundle>() {
+ return new ResultSet<>() {
@Override
public Iterator<FieldBundle> iterator() {
return fieldBundles.iterator();
diff --git a/java/com/google/gerrit/lucene/QueryBuilder.java b/java/com/google/gerrit/lucene/QueryBuilder.java
index 7d82bf5..e1b56c6 100644
--- a/java/com/google/gerrit/lucene/QueryBuilder.java
+++ b/java/com/google/gerrit/lucene/QueryBuilder.java
@@ -33,7 +33,6 @@
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.index.query.RegexPredicate;
import com.google.gerrit.index.query.TimestampRangePredicate;
-import java.util.Date;
import java.util.List;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.IntPoint;
@@ -232,15 +231,17 @@
if (p instanceof TimestampRangePredicate) {
TimestampRangePredicate<V> r = (TimestampRangePredicate<V>) p;
return longRangeQuery.get(
- r.getField().getName(), r.getMinTimestamp().getTime(), r.getMaxTimestamp().getTime());
+ r.getField().getName(),
+ r.getMinTimestamp().toEpochMilli(),
+ r.getMaxTimestamp().toEpochMilli());
}
throw new QueryParseException("not a timestamp: " + p);
}
private Query notTimestamp(TimestampRangePredicate<V> r) throws QueryParseException {
- if (r.getMinTimestamp().getTime() == 0) {
+ if (r.getMinTimestamp().toEpochMilli() == 0) {
return longRangeQuery.get(
- r.getField().getName(), r.getMaxTimestamp().getTime(), Long.MAX_VALUE);
+ r.getField().getName(), r.getMaxTimestamp().toEpochMilli(), Long.MAX_VALUE);
}
throw new QueryParseException("cannot negate: " + r);
}
@@ -279,10 +280,6 @@
return query;
}
- public int toIndexTimeInMinutes(Date ts) {
- return (int) (ts.getTime() / 60000);
- }
-
public Schema<V> getSchema() {
return schema;
}
diff --git a/java/com/google/gerrit/mail/BUILD b/java/com/google/gerrit/mail/BUILD
index 59d8227..0fe6c43 100644
--- a/java/com/google/gerrit/mail/BUILD
+++ b/java/com/google/gerrit/mail/BUILD
@@ -12,7 +12,6 @@
"//lib/auto:auto-value-annotations",
"//lib/flogger:api",
"//lib/jsoup",
- "//lib/log:log4j",
"//lib/mime4j:core",
"//lib/mime4j:dom",
],
diff --git a/java/com/google/gerrit/mail/RawMailParser.java b/java/com/google/gerrit/mail/RawMailParser.java
index 213cc3f..929e9f9 100644
--- a/java/com/google/gerrit/mail/RawMailParser.java
+++ b/java/com/google/gerrit/mail/RawMailParser.java
@@ -25,6 +25,7 @@
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
+import java.time.Instant;
import org.apache.james.mime4j.MimeException;
import org.apache.james.mime4j.dom.Entity;
import org.apache.james.mime4j.dom.Message;
@@ -66,7 +67,9 @@
messageBuilder.subject(mimeMessage.getSubject());
}
if (mimeMessage.getDate() != null) {
- messageBuilder.dateReceived(mimeMessage.getDate().toInstant());
+ @SuppressWarnings("JdkObsolete")
+ Instant mimeMessageInstant = mimeMessage.getDate().toInstant();
+ messageBuilder.dateReceived(mimeMessageInstant);
}
// Add From, To and Cc
diff --git a/java/com/google/gerrit/metrics/DisabledMetricMaker.java b/java/com/google/gerrit/metrics/DisabledMetricMaker.java
index 1fb8c57..234378b 100644
--- a/java/com/google/gerrit/metrics/DisabledMetricMaker.java
+++ b/java/com/google/gerrit/metrics/DisabledMetricMaker.java
@@ -33,7 +33,7 @@
@Override
public <F1> Counter1<F1> newCounter(String name, Description desc, Field<F1> field1) {
- return new Counter1<F1>() {
+ return new Counter1<>() {
@Override
public void incrementBy(F1 field1, long value) {}
@@ -45,7 +45,7 @@
@Override
public <F1, F2> Counter2<F1, F2> newCounter(
String name, Description desc, Field<F1> field1, Field<F2> field2) {
- return new Counter2<F1, F2>() {
+ return new Counter2<>() {
@Override
public void incrementBy(F1 field1, F2 field2, long value) {}
@@ -57,7 +57,7 @@
@Override
public <F1, F2, F3> Counter3<F1, F2, F3> newCounter(
String name, Description desc, Field<F1> field1, Field<F2> field2, Field<F3> field3) {
- return new Counter3<F1, F2, F3>() {
+ return new Counter3<>() {
@Override
public void incrementBy(F1 field1, F2 field2, F3 field3, long value) {}
@@ -79,7 +79,7 @@
@Override
public <F1> Timer1<F1> newTimer(String name, Description desc, Field<F1> field1) {
- return new Timer1<F1>(name, field1) {
+ return new Timer1<>(name, field1) {
@Override
protected void doRecord(F1 field1, long value, TimeUnit unit) {}
@@ -91,7 +91,7 @@
@Override
public <F1, F2> Timer2<F1, F2> newTimer(
String name, Description desc, Field<F1> field1, Field<F2> field2) {
- return new Timer2<F1, F2>(name, field1, field2) {
+ return new Timer2<>(name, field1, field2) {
@Override
protected void doRecord(F1 field1, F2 field2, long value, TimeUnit unit) {}
@@ -103,7 +103,7 @@
@Override
public <F1, F2, F3> Timer3<F1, F2, F3> newTimer(
String name, Description desc, Field<F1> field1, Field<F2> field2, Field<F3> field3) {
- return new Timer3<F1, F2, F3>(name, field1, field2, field3) {
+ return new Timer3<>(name, field1, field2, field3) {
@Override
protected void doRecord(F1 field1, F2 field2, F3 field3, long value, TimeUnit unit) {}
@@ -125,7 +125,7 @@
@Override
public <F1> Histogram1<F1> newHistogram(String name, Description desc, Field<F1> field1) {
- return new Histogram1<F1>() {
+ return new Histogram1<>() {
@Override
public void record(F1 field1, long value) {}
@@ -137,7 +137,7 @@
@Override
public <F1, F2> Histogram2<F1, F2> newHistogram(
String name, Description desc, Field<F1> field1, Field<F2> field2) {
- return new Histogram2<F1, F2>() {
+ return new Histogram2<>() {
@Override
public void record(F1 field1, F2 field2, long value) {}
@@ -149,7 +149,7 @@
@Override
public <F1, F2, F3> Histogram3<F1, F2, F3> newHistogram(
String name, Description desc, Field<F1> field1, Field<F2> field2, Field<F3> field3) {
- return new Histogram3<F1, F2, F3>() {
+ return new Histogram3<>() {
@Override
public void record(F1 field1, F2 field2, F3 field3, long value) {}
@@ -161,7 +161,7 @@
@Override
public <V> CallbackMetric0<V> newCallbackMetric(
String name, Class<V> valueClass, Description desc) {
- return new CallbackMetric0<V>() {
+ return new CallbackMetric0<>() {
@Override
public void set(V value) {}
@@ -173,7 +173,7 @@
@Override
public <F1, V> CallbackMetric1<F1, V> newCallbackMetric(
String name, Class<V> valueClass, Description desc, Field<F1> field1) {
- return new CallbackMetric1<F1, V>() {
+ return new CallbackMetric1<>() {
@Override
public void set(F1 field1, V value) {}
diff --git a/java/com/google/gerrit/metrics/dropwizard/CounterImpl1.java b/java/com/google/gerrit/metrics/dropwizard/CounterImpl1.java
index 0e554a8..92aeb4c 100644
--- a/java/com/google/gerrit/metrics/dropwizard/CounterImpl1.java
+++ b/java/com/google/gerrit/metrics/dropwizard/CounterImpl1.java
@@ -26,7 +26,7 @@
}
Counter1<F1> counter() {
- return new Counter1<F1>() {
+ return new Counter1<>() {
@Override
public void incrementBy(F1 field1, long value) {
total.incrementBy(value);
diff --git a/java/com/google/gerrit/metrics/dropwizard/CounterImplN.java b/java/com/google/gerrit/metrics/dropwizard/CounterImplN.java
index 07afc2a..e9199d9 100644
--- a/java/com/google/gerrit/metrics/dropwizard/CounterImplN.java
+++ b/java/com/google/gerrit/metrics/dropwizard/CounterImplN.java
@@ -29,7 +29,7 @@
}
<F1, F2> Counter2<F1, F2> counter2() {
- return new Counter2<F1, F2>() {
+ return new Counter2<>() {
@Override
public void incrementBy(F1 field1, F2 field2, long value) {
total.incrementBy(value);
@@ -44,7 +44,7 @@
}
<F1, F2, F3> Counter3<F1, F2, F3> counter3() {
- return new Counter3<F1, F2, F3>() {
+ return new Counter3<>() {
@Override
public void incrementBy(F1 field1, F2 field2, F3 field3, long value) {
total.incrementBy(value);
diff --git a/java/com/google/gerrit/metrics/dropwizard/HistogramImpl1.java b/java/com/google/gerrit/metrics/dropwizard/HistogramImpl1.java
index 4578db1..91e36b9 100644
--- a/java/com/google/gerrit/metrics/dropwizard/HistogramImpl1.java
+++ b/java/com/google/gerrit/metrics/dropwizard/HistogramImpl1.java
@@ -26,7 +26,7 @@
}
Histogram1<F1> histogram1() {
- return new Histogram1<F1>() {
+ return new Histogram1<>() {
@Override
public void record(F1 field1, long value) {
total.record(value);
diff --git a/java/com/google/gerrit/metrics/dropwizard/HistogramImplN.java b/java/com/google/gerrit/metrics/dropwizard/HistogramImplN.java
index 446590c..2caa4c5 100644
--- a/java/com/google/gerrit/metrics/dropwizard/HistogramImplN.java
+++ b/java/com/google/gerrit/metrics/dropwizard/HistogramImplN.java
@@ -29,7 +29,7 @@
}
<F1, F2> Histogram2<F1, F2> histogram2() {
- return new Histogram2<F1, F2>() {
+ return new Histogram2<>() {
@Override
public void record(F1 field1, F2 field2, long value) {
total.record(value);
@@ -44,7 +44,7 @@
}
<F1, F2, F3> Histogram3<F1, F2, F3> histogram3() {
- return new Histogram3<F1, F2, F3>() {
+ return new Histogram3<>() {
@Override
public void record(F1 field1, F2 field2, F3 field3, long value) {
total.record(value);
diff --git a/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java b/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java
index 7e472c9..6b17456 100644
--- a/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java
+++ b/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java
@@ -26,7 +26,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
-import java.util.SortedMap;
+import java.util.NavigableMap;
import java.util.TreeMap;
import org.kohsuke.args4j.Option;
@@ -55,7 +55,7 @@
throws AuthException, PermissionBackendException {
permissionBackend.currentUser().check(GlobalPermission.VIEW_CACHES);
- SortedMap<String, MetricJson> out = new TreeMap<>();
+ NavigableMap<String, MetricJson> out = new TreeMap<>();
List<String> prefixes = new ArrayList<>(query.size());
for (String q : query) {
if (q.endsWith("/")) {
diff --git a/java/com/google/gerrit/metrics/dropwizard/MetricResource.java b/java/com/google/gerrit/metrics/dropwizard/MetricResource.java
index 226edc7..8a6db67 100644
--- a/java/com/google/gerrit/metrics/dropwizard/MetricResource.java
+++ b/java/com/google/gerrit/metrics/dropwizard/MetricResource.java
@@ -20,8 +20,7 @@
import com.google.inject.TypeLiteral;
class MetricResource extends ConfigResource {
- static final TypeLiteral<RestView<MetricResource>> METRIC_KIND =
- new TypeLiteral<RestView<MetricResource>>() {};
+ static final TypeLiteral<RestView<MetricResource>> METRIC_KIND = new TypeLiteral<>() {};
private final String name;
private final Metric metric;
diff --git a/java/com/google/gerrit/metrics/dropwizard/TimerImpl1.java b/java/com/google/gerrit/metrics/dropwizard/TimerImpl1.java
index b7d535b..36b52e1 100644
--- a/java/com/google/gerrit/metrics/dropwizard/TimerImpl1.java
+++ b/java/com/google/gerrit/metrics/dropwizard/TimerImpl1.java
@@ -28,7 +28,7 @@
@SuppressWarnings("unchecked")
Timer1<F1> timer() {
- return new Timer1<F1>(name, (Field<F1>) fields[0]) {
+ return new Timer1<>(name, (Field<F1>) fields[0]) {
@Override
protected void doRecord(F1 field1, long value, TimeUnit unit) {
total.record(value, unit);
diff --git a/java/com/google/gerrit/metrics/dropwizard/TimerImplN.java b/java/com/google/gerrit/metrics/dropwizard/TimerImplN.java
index dee800e..77ce8cd 100644
--- a/java/com/google/gerrit/metrics/dropwizard/TimerImplN.java
+++ b/java/com/google/gerrit/metrics/dropwizard/TimerImplN.java
@@ -31,7 +31,7 @@
@SuppressWarnings("unchecked")
<F1, F2> Timer2<F1, F2> timer2() {
- return new Timer2<F1, F2>(name, (Field<F1>) fields[0], (Field<F2>) fields[1]) {
+ return new Timer2<>(name, (Field<F1>) fields[0], (Field<F2>) fields[1]) {
@Override
protected void doRecord(F1 field1, F2 field2, long value, TimeUnit unit) {
total.record(value, unit);
@@ -47,8 +47,7 @@
@SuppressWarnings("unchecked")
<F1, F2, F3> Timer3<F1, F2, F3> timer3() {
- return new Timer3<F1, F2, F3>(
- name, (Field<F1>) fields[0], (Field<F2>) fields[1], (Field<F3>) fields[2]) {
+ return new Timer3<>(name, (Field<F1>) fields[0], (Field<F2>) fields[1], (Field<F3>) fields[2]) {
@Override
protected void doRecord(F1 field1, F2 field2, F3 field3, long value, TimeUnit unit) {
total.record(value, unit);
diff --git a/java/com/google/gerrit/pgm/BUILD b/java/com/google/gerrit/pgm/BUILD
index 3a1de5e..d4c5a87 100644
--- a/java/com/google/gerrit/pgm/BUILD
+++ b/java/com/google/gerrit/pgm/BUILD
@@ -53,7 +53,6 @@
"//lib/guice",
"//lib/guice:guice-assistedinject",
"//lib/guice:guice-servlet",
- "//lib/log:log4j",
"//lib/prolog:cafeteria",
"//lib/prolog:compiler",
"//lib/prolog:runtime",
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index d77daa1..75891fe 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -493,7 +493,7 @@
});
}
modules.add(new DefaultUrlFormatterModule());
- SshSessionFactoryInitializer.init(config);
+ SshSessionFactoryInitializer.init();
if (sshd) {
modules.add(SshKeyCacheImpl.module());
} else {
diff --git a/java/com/google/gerrit/pgm/JythonShell.java b/java/com/google/gerrit/pgm/JythonShell.java
index 88f7b5d..d85bdc0 100644
--- a/java/com/google/gerrit/pgm/JythonShell.java
+++ b/java/com/google/gerrit/pgm/JythonShell.java
@@ -173,7 +173,7 @@
logger.atSevere().log("Cannot load resource %s", p);
}
} catch (IOException e) {
- logger.atSevere().withCause(e).log(e.getMessage());
+ logger.atSevere().withCause(e).log("%s", e.getMessage());
}
}
diff --git a/java/com/google/gerrit/pgm/Ls.java b/java/com/google/gerrit/pgm/Ls.java
index 4211c17..4b48148 100644
--- a/java/com/google/gerrit/pgm/Ls.java
+++ b/java/com/google/gerrit/pgm/Ls.java
@@ -14,10 +14,11 @@
package com.google.gerrit.pgm;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
import com.google.gerrit.launcher.GerritLauncher;
import com.google.gerrit.pgm.util.AbstractProgram;
import java.io.IOException;
-import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
@@ -26,9 +27,7 @@
@Override
public int run() throws IOException {
try (ZipFile zf = new ZipFile(GerritLauncher.getDistributionArchive())) {
- final Enumeration<? extends ZipEntry> e = zf.entries();
- while (e.hasMoreElements()) {
- final ZipEntry ze = e.nextElement();
+ for (ZipEntry ze : entriesOf(zf)) {
String name = ze.getName();
boolean show = false;
show |= name.startsWith("WEB-INF/");
@@ -49,4 +48,8 @@
}
return 0;
}
+
+ private static Iterable<? extends ZipEntry> entriesOf(ZipFile zipFile) {
+ return zipFile.stream().collect(toImmutableList());
+ }
}
diff --git a/java/com/google/gerrit/pgm/SwitchSecureStore.java b/java/com/google/gerrit/pgm/SwitchSecureStore.java
index 733c9d1..824a9a7 100644
--- a/java/com/google/gerrit/pgm/SwitchSecureStore.java
+++ b/java/com/google/gerrit/pgm/SwitchSecureStore.java
@@ -196,7 +196,7 @@
return jar;
}
} catch (IOException e) {
- logger.atSevere().withCause(e).log(e.getMessage());
+ logger.atSevere().withCause(e).log("%s", e.getMessage());
}
}
return null;
diff --git a/java/com/google/gerrit/pgm/WarDistribution.java b/java/com/google/gerrit/pgm/WarDistribution.java
index 257fb4e..013c850 100644
--- a/java/com/google/gerrit/pgm/WarDistribution.java
+++ b/java/com/google/gerrit/pgm/WarDistribution.java
@@ -14,6 +14,7 @@
package com.google.gerrit.pgm;
+import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.pgm.init.InitPlugins.JAR;
import static com.google.gerrit.pgm.init.InitPlugins.PLUGIN_DIR;
@@ -25,7 +26,6 @@
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
-import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
@@ -39,9 +39,7 @@
File myWar = GerritLauncher.getDistributionArchive();
if (myWar.isFile()) {
try (ZipFile zf = new ZipFile(myWar)) {
- Enumeration<? extends ZipEntry> e = zf.entries();
- while (e.hasMoreElements()) {
- ZipEntry ze = e.nextElement();
+ for (ZipEntry ze : entriesOf(zf)) {
if (ze.isDirectory()) {
continue;
}
@@ -65,4 +63,8 @@
// not yet used
throw new UnsupportedOperationException();
}
+
+ private static Iterable<? extends ZipEntry> entriesOf(ZipFile zipFile) {
+ return zipFile.stream().collect(toImmutableList());
+ }
}
diff --git a/java/com/google/gerrit/pgm/init/AccountsOnInit.java b/java/com/google/gerrit/pgm/init/AccountsOnInit.java
index d9e3a6a..cbfd714 100644
--- a/java/com/google/gerrit/pgm/init/AccountsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/AccountsOnInit.java
@@ -30,6 +30,7 @@
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
+import java.util.Date;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheEditor;
import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
@@ -60,12 +61,16 @@
this.allUsers = allUsers.get();
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
public Account insert(Account.Builder account) throws IOException {
File path = getPath();
try (Repository repo = new FileRepository(path);
ObjectInserter oi = repo.newObjectInserter()) {
PersonIdent ident =
- new PersonIdent(new GerritPersonIdentProvider(flags.cfg).get(), account.registeredOn());
+ new PersonIdent(
+ new GerritPersonIdentProvider(flags.cfg).get(), Date.from(account.registeredOn()));
Config accountConfig = new Config();
AccountProperties.writeToAccountConfig(
diff --git a/java/com/google/gerrit/pgm/init/BaseInit.java b/java/com/google/gerrit/pgm/init/BaseInit.java
index c1f5753..4592cbb 100644
--- a/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -124,7 +124,7 @@
} catch (StorageException e) {
String msg = "Couldn't upgrade schema. Expected if slave and read-only database";
System.err.println(msg);
- logger.atSevere().withCause(e).log(msg);
+ logger.atSevere().withCause(e).log("%s", msg);
}
init.initializer.postRun(sysInjector);
diff --git a/java/com/google/gerrit/pgm/init/GroupsOnInit.java b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
index 2f12abb..020705e 100644
--- a/java/com/google/gerrit/pgm/init/GroupsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
@@ -41,6 +41,7 @@
import java.io.IOException;
import java.nio.file.Path;
import java.sql.Timestamp;
+import java.time.Instant;
import java.util.stream.Stream;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.internal.storage.file.FileRepository;
@@ -165,10 +166,14 @@
return AuditLogFormatter.createBackedBy(ImmutableSet.of(account), ImmutableSet.of(), serverId);
}
- private void commit(Repository repository, GroupConfig groupConfig, Timestamp groupCreatedOn)
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
+ private void commit(Repository repository, GroupConfig groupConfig, Instant groupCreatedOn)
throws IOException {
PersonIdent personIdent =
- new PersonIdent(new GerritPersonIdentProvider(flags.cfg).get(), groupCreatedOn);
+ new PersonIdent(
+ new GerritPersonIdentProvider(flags.cfg).get(), Timestamp.from(groupCreatedOn));
try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate(repository, personIdent)) {
groupConfig.commit(metaDataUpdate);
}
diff --git a/java/com/google/gerrit/pgm/init/InitAdminUser.java b/java/com/google/gerrit/pgm/init/InitAdminUser.java
index d6a0133..4e854b5 100644
--- a/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -120,7 +120,7 @@
Account persistedAccount =
accounts.insert(
- Account.builder(id, TimeUtil.nowTs()).setFullName(name).setPreferredEmail(email));
+ Account.builder(id, TimeUtil.now()).setFullName(name).setPreferredEmail(email));
// Only two groups should exist at this point in time and hence iterating over all of them
// is cheap.
Optional<GroupReference> adminGroupReference =
diff --git a/java/com/google/gerrit/pgm/init/api/GitRepositoryManagerOnInit.java b/java/com/google/gerrit/pgm/init/api/GitRepositoryManagerOnInit.java
index 8e69eb9..fabad49 100644
--- a/java/com/google/gerrit/pgm/init/api/GitRepositoryManagerOnInit.java
+++ b/java/com/google/gerrit/pgm/init/api/GitRepositoryManagerOnInit.java
@@ -23,7 +23,7 @@
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
-import java.util.SortedSet;
+import java.util.NavigableSet;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.lib.Repository;
@@ -65,7 +65,7 @@
}
@Override
- public SortedSet<Project.NameKey> list() {
+ public NavigableSet<Project.NameKey> list() {
throw new UnsupportedOperationException("not implemented");
}
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index 9bd3067..ae9e72f 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -20,6 +20,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
import com.google.gerrit.extensions.common.AccountVisibility;
import com.google.gerrit.extensions.config.FactoryModule;
@@ -40,7 +41,6 @@
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.account.ServiceUserClassifierImpl;
import com.google.gerrit.server.account.externalids.ExternalIdCacheModule;
-import com.google.gerrit.server.approval.ApprovalCacheImpl;
import com.google.gerrit.server.cache.CacheRemovalListener;
import com.google.gerrit.server.cache.h2.H2CacheModule;
import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
@@ -176,7 +176,6 @@
modules.add(new GroupModule());
modules.add(new NoteDbModule());
modules.add(AccountCacheImpl.module());
- modules.add(ApprovalCacheImpl.module());
modules.add(ConflictsCacheImpl.module());
modules.add(DefaultPreferencesCacheImpl.module());
modules.add(GroupCacheImpl.module());
@@ -206,6 +205,9 @@
modules.add(new DefaultSubmitRuleModule());
modules.add(new IgnoreSelfApprovalRuleModule());
+ // Global submit requirements
+ DynamicSet.setOf(binder(), SubmitRequirement.class);
+
bind(ChangeJson.Factory.class).toProvider(Providers.of(null));
bind(EventUtil.class).toProvider(Providers.of(null));
bind(GitReferenceUpdated.class).toInstance(GitReferenceUpdated.DISABLED);
diff --git a/java/com/google/gerrit/pgm/util/ProxyUtil.java b/java/com/google/gerrit/pgm/util/ProxyUtil.java
index 3eb8187..c2c1141 100644
--- a/java/com/google/gerrit/pgm/util/ProxyUtil.java
+++ b/java/com/google/gerrit/pgm/util/ProxyUtil.java
@@ -59,7 +59,7 @@
return;
}
- final URL u = new URL((!s.contains("://")) ? "http://" + s : s);
+ final URL u = new URL(!s.contains("://") ? "http://" + s : s);
if (!"http".equals(u.getProtocol())) {
throw new MalformedURLException("Invalid http_proxy: " + s + ": Only http supported.");
}
diff --git a/java/com/google/gerrit/server/AssigneeStatusUpdate.java b/java/com/google/gerrit/server/AssigneeStatusUpdate.java
index 3d6242b..812aad1 100644
--- a/java/com/google/gerrit/server/AssigneeStatusUpdate.java
+++ b/java/com/google/gerrit/server/AssigneeStatusUpdate.java
@@ -16,18 +16,18 @@
import com.google.auto.value.AutoValue;
import com.google.gerrit.entities.Account;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Optional;
/** Change to an assignee's status. */
@AutoValue
public abstract class AssigneeStatusUpdate {
public static AssigneeStatusUpdate create(
- Timestamp ts, Account.Id updatedBy, Optional<Account.Id> currentAssignee) {
+ Instant ts, Account.Id updatedBy, Optional<Account.Id> currentAssignee) {
return new AutoValue_AssigneeStatusUpdate(ts, updatedBy, currentAssignee);
}
- public abstract Timestamp date();
+ public abstract Instant date();
public abstract Account.Id updatedBy();
diff --git a/java/com/google/gerrit/server/AuditEvent.java b/java/com/google/gerrit/server/AuditEvent.java
index 773a307..54bbe23 100644
--- a/java/com/google/gerrit/server/AuditEvent.java
+++ b/java/com/google/gerrit/server/AuditEvent.java
@@ -82,6 +82,12 @@
return uuid.hashCode();
}
+ // This is a value class that allows adding attributes by subclassing.
+ // Doing this is discouraged and using composition rather than inheritance to add fields to value
+ // types is preferred. However this class is part of the plugin API (used in the AuditListener
+ // extension point), hence we cannot change it without breaking plugins. Hence suppress the
+ // EqualsGetClass warning here.
+ @SuppressWarnings("EqualsGetClass")
@Override
public boolean equals(Object obj) {
if (this == obj) {
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index 7080417..9470931 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -144,6 +144,7 @@
visibility = ["//visibility:public"],
deps = [
":server",
+ "//java/com/google/gerrit/entities",
"//java/com/google/gerrit/extensions:api",
"//java/com/google/gerrit/server/git/receive",
"//java/com/google/gerrit/server/logging",
diff --git a/java/com/google/gerrit/server/ChangeMessagesUtil.java b/java/com/google/gerrit/server/ChangeMessagesUtil.java
index 8366b09..81cff6e 100644
--- a/java/com/google/gerrit/server/ChangeMessagesUtil.java
+++ b/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -145,7 +145,7 @@
ChangeMessageInfo cmi = new ChangeMessageInfo();
cmi.id = message.getKey().uuid();
cmi.author = accountLoader.get(message.getAuthor());
- cmi.date = message.getWrittenOn();
+ cmi.setDate(message.getWrittenOn());
cmi.message = message.getMessage();
cmi.tag = message.getTag();
cmi._revisionNumber = patchNum != null ? patchNum.get() : null;
diff --git a/java/com/google/gerrit/server/CmdLineParserModule.java b/java/com/google/gerrit/server/CmdLineParserModule.java
index d943889..be6b4cd8 100644
--- a/java/com/google/gerrit/server/CmdLineParserModule.java
+++ b/java/com/google/gerrit/server/CmdLineParserModule.java
@@ -23,17 +23,17 @@
import com.google.gerrit.server.args4j.AccountGroupUUIDHandler;
import com.google.gerrit.server.args4j.AccountIdHandler;
import com.google.gerrit.server.args4j.ChangeIdHandler;
+import com.google.gerrit.server.args4j.InstantHandler;
import com.google.gerrit.server.args4j.ObjectIdHandler;
import com.google.gerrit.server.args4j.PatchSetIdHandler;
import com.google.gerrit.server.args4j.ProjectHandler;
import com.google.gerrit.server.args4j.SocketAddressHandler;
-import com.google.gerrit.server.args4j.TimestampHandler;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.util.cli.CmdLineParser;
import com.google.gerrit.util.cli.OptionHandlerUtil;
import com.google.gerrit.util.cli.OptionHandlers;
import java.net.SocketAddress;
-import java.sql.Timestamp;
+import java.time.Instant;
import org.eclipse.jgit.lib.ObjectId;
import org.kohsuke.args4j.spi.OptionHandler;
@@ -49,11 +49,11 @@
registerOptionHandler(AccountGroup.Id.class, AccountGroupIdHandler.class);
registerOptionHandler(AccountGroup.UUID.class, AccountGroupUUIDHandler.class);
registerOptionHandler(Change.Id.class, ChangeIdHandler.class);
+ registerOptionHandler(Instant.class, InstantHandler.class);
registerOptionHandler(ObjectId.class, ObjectIdHandler.class);
registerOptionHandler(PatchSet.Id.class, PatchSetIdHandler.class);
registerOptionHandler(ProjectState.class, ProjectHandler.class);
registerOptionHandler(SocketAddress.class, SocketAddressHandler.class);
- registerOptionHandler(Timestamp.class, TimestampHandler.class);
}
private <T> void registerOptionHandler(Class<T> type, Class<? extends OptionHandler<T>> impl) {
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index ba9f6d6..8198ce4 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -44,17 +44,20 @@
import com.google.gerrit.server.notedb.ChangeUpdate;
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.gerrit.server.update.ChangeContext;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
@@ -64,7 +67,7 @@
@Singleton
public class CommentsUtil {
public static final Ordering<Comment> COMMENT_ORDER =
- new Ordering<Comment>() {
+ new Ordering<>() {
@Override
public int compare(Comment c1, Comment c2) {
return ComparisonChain.start()
@@ -78,7 +81,7 @@
};
public static final Ordering<CommentInfo> COMMENT_INFO_ORDER =
- new Ordering<CommentInfo>() {
+ new Ordering<>() {
@Override
public int compare(CommentInfo a, CommentInfo b) {
return ComparisonChain.start()
@@ -130,7 +133,7 @@
public HumanComment newHumanComment(
ChangeNotes changeNotes,
CurrentUser currentUser,
- Timestamp when,
+ Instant when,
String path,
PatchSet.Id psId,
short side,
@@ -302,7 +305,7 @@
}
private static boolean isAfter(CommentInfo c, ChangeMessage cm) {
- return c.updated.after(cm.getWrittenOn());
+ return c.getUpdated().isAfter(cm.getWrittenOn());
}
/**
@@ -335,7 +338,7 @@
}
public void putHumanComments(
- ChangeUpdate update, HumanComment.Status status, Iterable<HumanComment> comments) {
+ ChangeUpdate update, Comment.Status status, Iterable<HumanComment> comments) {
for (HumanComment c : comments) {
update.putComment(status, c);
}
@@ -438,7 +441,7 @@
// unignore the test in PortedCommentsIT.
Map<String, FileDiffOutput> modifiedFiles =
diffOperations.listModifiedFilesAgainstParent(
- change.getProject(), patchset.commitId(), /* parentNum= */ 0);
+ change.getProject(), patchset.commitId(), /* parentNum= */ 0, DiffOptions.DEFAULTS);
return modifiedFiles.isEmpty()
? null
: modifiedFiles.values().iterator().next().oldCommitId();
@@ -465,10 +468,35 @@
}
}
+ /** returns all changes that contain draft comments of {@code accountId}. */
+ public Collection<Change.Id> getChangesWithDrafts(Account.Id accountId) {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ return getChangesWithDrafts(repo, accountId);
+ } catch (IOException e) {
+ throw new StorageException(e);
+ }
+ }
+
private Collection<Ref> getDraftRefs(Repository repo, Change.Id changeId) throws IOException {
return repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftCommentsPrefix(changeId));
}
+ private Collection<Change.Id> getChangesWithDrafts(Repository repo, Account.Id accountId)
+ throws IOException {
+ Set<Change.Id> changes = new HashSet<>();
+ for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_DRAFT_COMMENTS)) {
+ Integer accountIdFromRef = RefNames.parseRefSuffix(ref.getName());
+ if (accountIdFromRef != null && accountIdFromRef == accountId.get()) {
+ Change.Id changeId = Change.Id.fromAllUsersRef(ref.getName());
+ if (changeId == null) {
+ continue;
+ }
+ changes.add(changeId);
+ }
+ }
+ return changes;
+ }
+
private static <T extends Comment> List<T> sort(List<T> comments) {
comments.sort(COMMENT_ORDER);
return comments;
diff --git a/java/com/google/gerrit/server/CommonConverters.java b/java/com/google/gerrit/server/CommonConverters.java
index 2b48169..e7fd1c5 100644
--- a/java/com/google/gerrit/server/CommonConverters.java
+++ b/java/com/google/gerrit/server/CommonConverters.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server;
import com.google.gerrit.extensions.common.GitPerson;
-import java.sql.Timestamp;
import org.eclipse.jgit.lib.PersonIdent;
/**
@@ -26,11 +25,14 @@
* static utility methods.
*/
public class CommonConverters {
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
public static GitPerson toGitPerson(PersonIdent ident) {
GitPerson result = new GitPerson();
result.name = ident.getName();
result.email = ident.getEmailAddress();
- result.date = new Timestamp(ident.getWhen().getTime());
+ result.setDate(ident.getWhen().toInstant());
result.tz = ident.getTimeZoneOffset();
return result;
}
diff --git a/java/com/google/gerrit/server/ExceptionHookImpl.java b/java/com/google/gerrit/server/ExceptionHookImpl.java
index 3986842..781f196 100644
--- a/java/com/google/gerrit/server/ExceptionHookImpl.java
+++ b/java/com/google/gerrit/server/ExceptionHookImpl.java
@@ -18,7 +18,7 @@
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.common.Nullable;
-import com.google.gerrit.exceptions.InternalServerWithUserMessageException;
+import com.google.gerrit.exceptions.MergeUpdateException;
import com.google.gerrit.git.LockFailureException;
import com.google.gerrit.server.project.ProjectConfig;
import java.util.Optional;
@@ -74,7 +74,7 @@
+ "\n"
+ CONTACT_PROJECT_OWNER_USER_MESSAGE);
}
- if (throwable instanceof InternalServerWithUserMessageException) {
+ if (throwable instanceof MergeUpdateException) {
return ImmutableList.of(throwable.getMessage());
}
return ImmutableList.of();
diff --git a/java/com/google/gerrit/server/IdentifiedUser.java b/java/com/google/gerrit/server/IdentifiedUser.java
index eb3e324..122e18d 100644
--- a/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/java/com/google/gerrit/server/IdentifiedUser.java
@@ -49,7 +49,7 @@
import java.net.MalformedURLException;
import java.net.SocketAddress;
import java.net.URL;
-import java.util.Date;
+import java.time.Instant;
import java.util.Optional;
import java.util.Set;
import java.util.TimeZone;
@@ -427,10 +427,10 @@
}
public PersonIdent newRefLogIdent() {
- return newRefLogIdent(new Date(), TimeZone.getDefault());
+ return newRefLogIdent(Instant.now(), TimeZone.getDefault());
}
- public PersonIdent newRefLogIdent(Date when, TimeZone tz) {
+ public PersonIdent newRefLogIdent(Instant when, TimeZone tz) {
final Account ua = getAccount();
String name = ua.fullName();
@@ -450,14 +450,22 @@
? constructMailAddress(ua, "unknown")
: ua.preferredEmail();
}
- return new PersonIdent(name, user, when, tz);
+
+ return newPersonIdent(name, user, when, tz);
}
private String constructMailAddress(Account ua, String host) {
return getUserName().orElse("") + "|account-" + ua.id().toString() + "@" + host;
}
- public PersonIdent newCommitterIdent(Date when, TimeZone tz) {
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
+ public PersonIdent newCommitterIdent(PersonIdent ident) {
+ return newCommitterIdent(ident.getWhen().toInstant(), ident.getTimeZone());
+ }
+
+ public PersonIdent newCommitterIdent(Instant when, TimeZone tz) {
final Account ua = getAccount();
String name = ua.fullName();
String email = ua.preferredEmail();
@@ -492,7 +500,7 @@
}
}
- return new PersonIdent(name, email, when, tz);
+ return newPersonIdent(name, email, when, tz);
}
@Override
@@ -560,4 +568,19 @@
}
return host;
}
+
+ /**
+ * Create a {@link PersonIdent} from an {@code Instant} and a {@link TimeZone}.
+ *
+ * <p>We use the {@link PersonIdent#PersonIdent(String, String, long, int)} constructor to avoid
+ * doing a conversion to {@code java.util.Date} here. For the {@code int aTZ} argument, which is
+ * the time zone, we do the same computation as in {@link PersonIdent#PersonIdent(String, String,
+ * java.util.Date, TimeZone)} (just instead of getting the epoch millis from {@code
+ * java.util.Date} we get them from {@link Instant}).
+ */
+ // TODO(issue-15517): Drop this method once JGit's PersonIdent class supports Instants
+ private static PersonIdent newPersonIdent(String name, String email, Instant when, TimeZone tz) {
+ return new PersonIdent(
+ name, email, when.toEpochMilli(), tz.getOffset(when.toEpochMilli()) / (60 * 1000));
+ }
}
diff --git a/java/com/google/gerrit/server/LibModuleLoader.java b/java/com/google/gerrit/server/LibModuleLoader.java
index 36765e9..cf53c80 100644
--- a/java/com/google/gerrit/server/LibModuleLoader.java
+++ b/java/com/google/gerrit/server/LibModuleLoader.java
@@ -81,9 +81,7 @@
try {
return (Class<Module>) Class.forName(className);
} catch (ClassNotFoundException | LinkageError e) {
- String msg = "Cannot load LibModule " + className;
- logger.atSevere().withCause(e).log(msg);
- throw new ProvisionException(msg, e);
+ throw new ProvisionException("Cannot load LibModule " + className, e);
}
}
}
diff --git a/java/com/google/gerrit/server/PatchSetUtil.java b/java/com/google/gerrit/server/PatchSetUtil.java
index 326ddf4..3d449b7 100644
--- a/java/com/google/gerrit/server/PatchSetUtil.java
+++ b/java/com/google/gerrit/server/PatchSetUtil.java
@@ -39,7 +39,6 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -106,7 +105,7 @@
.id(psId)
.commitId(commit)
.uploader(update.getAccountId())
- .createdOn(new Timestamp(update.getWhen().getTime()))
+ .createdOn(update.getWhen())
.groups(groups)
.pushCertificate(Optional.ofNullable(pushCertificate))
.description(Optional.ofNullable(description))
diff --git a/java/com/google/gerrit/server/PublishCommentUtil.java b/java/com/google/gerrit/server/PublishCommentUtil.java
index 4d19dd0..de5f023 100644
--- a/java/com/google/gerrit/server/PublishCommentUtil.java
+++ b/java/com/google/gerrit/server/PublishCommentUtil.java
@@ -32,6 +32,7 @@
import com.google.gerrit.server.update.ChangeContext;
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import java.sql.Timestamp;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
@@ -90,7 +91,7 @@
draftComment, psIdOfDraftComment, notes.getProjectName());
continue;
}
- draftComment.writtenOn = ctx.getWhen();
+ draftComment.writtenOn = Timestamp.from(ctx.getWhen());
draftComment.tag = tag;
// Draft may have been created by a different real user; copy the current real user. (Only
// applies to X-Gerrit-RunAs, since modifying drafts via on_behalf_of is not allowed.)
diff --git a/java/com/google/gerrit/server/RequestCleanup.java b/java/com/google/gerrit/server/RequestCleanup.java
index e07d148..1d421ed 100644
--- a/java/com/google/gerrit/server/RequestCleanup.java
+++ b/java/com/google/gerrit/server/RequestCleanup.java
@@ -16,8 +16,8 @@
import com.google.common.flogger.FluentLogger;
import com.google.inject.servlet.RequestScoped;
+import java.util.ArrayList;
import java.util.Iterator;
-import java.util.LinkedList;
import java.util.List;
/** Registers cleanup activities to be completed when a scope ends. */
@@ -25,7 +25,7 @@
public class RequestCleanup implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- private final List<Runnable> cleanup = new LinkedList<>();
+ private final List<Runnable> cleanup = new ArrayList<>();
private boolean ran;
/** Register a task to be completed after the request ends. */
diff --git a/java/com/google/gerrit/server/ReviewerByEmailSet.java b/java/com/google/gerrit/server/ReviewerByEmailSet.java
index 4a317c3..c6ba7b5 100644
--- a/java/com/google/gerrit/server/ReviewerByEmailSet.java
+++ b/java/com/google/gerrit/server/ReviewerByEmailSet.java
@@ -19,7 +19,7 @@
import com.google.common.collect.Table;
import com.google.gerrit.entities.Address;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
-import java.sql.Timestamp;
+import java.time.Instant;
/**
* Set of reviewers on a change that do not have a Gerrit account and were added by email instead.
@@ -30,8 +30,7 @@
public class ReviewerByEmailSet {
private static final ReviewerByEmailSet EMPTY = new ReviewerByEmailSet(ImmutableTable.of());
- public static ReviewerByEmailSet fromTable(
- Table<ReviewerStateInternal, Address, Timestamp> table) {
+ public static ReviewerByEmailSet fromTable(Table<ReviewerStateInternal, Address, Instant> table) {
return new ReviewerByEmailSet(table);
}
@@ -39,10 +38,10 @@
return EMPTY;
}
- private final ImmutableTable<ReviewerStateInternal, Address, Timestamp> table;
+ private final ImmutableTable<ReviewerStateInternal, Address, Instant> table;
private ImmutableSet<Address> users;
- private ReviewerByEmailSet(Table<ReviewerStateInternal, Address, Timestamp> table) {
+ private ReviewerByEmailSet(Table<ReviewerStateInternal, Address, Instant> table) {
this.table = ImmutableTable.copyOf(table);
}
@@ -58,7 +57,7 @@
return table.row(state).keySet();
}
- public ImmutableTable<ReviewerStateInternal, Address, Timestamp> asTable() {
+ public ImmutableTable<ReviewerStateInternal, Address, Instant> asTable() {
return table;
}
diff --git a/java/com/google/gerrit/server/ReviewerSet.java b/java/com/google/gerrit/server/ReviewerSet.java
index 0f6bf29..0ff68e0 100644
--- a/java/com/google/gerrit/server/ReviewerSet.java
+++ b/java/com/google/gerrit/server/ReviewerSet.java
@@ -25,7 +25,7 @@
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
-import java.sql.Timestamp;
+import java.time.Instant;
/**
* Set of reviewers on a change.
@@ -38,7 +38,7 @@
public static ReviewerSet fromApprovals(Iterable<PatchSetApproval> approvals) {
PatchSetApproval first = null;
- Table<ReviewerStateInternal, Account.Id, Timestamp> reviewers = HashBasedTable.create();
+ Table<ReviewerStateInternal, Account.Id, Instant> reviewers = HashBasedTable.create();
for (PatchSetApproval psa : approvals) {
if (first == null) {
first = psa;
@@ -58,7 +58,7 @@
return new ReviewerSet(reviewers);
}
- public static ReviewerSet fromTable(Table<ReviewerStateInternal, Account.Id, Timestamp> table) {
+ public static ReviewerSet fromTable(Table<ReviewerStateInternal, Account.Id, Instant> table) {
return new ReviewerSet(table);
}
@@ -66,10 +66,10 @@
return EMPTY;
}
- private final ImmutableTable<ReviewerStateInternal, Account.Id, Timestamp> table;
+ private final ImmutableTable<ReviewerStateInternal, Account.Id, Instant> table;
private ImmutableSet<Account.Id> accounts;
- private ReviewerSet(Table<ReviewerStateInternal, Account.Id, Timestamp> table) {
+ private ReviewerSet(Table<ReviewerStateInternal, Account.Id, Instant> table) {
this.table = ImmutableTable.copyOf(table);
}
@@ -85,7 +85,7 @@
return table.row(state).keySet();
}
- public ImmutableTable<ReviewerStateInternal, Account.Id, Timestamp> asTable() {
+ public ImmutableTable<ReviewerStateInternal, Account.Id, Instant> asTable() {
return table;
}
diff --git a/java/com/google/gerrit/server/ReviewerStatusUpdate.java b/java/com/google/gerrit/server/ReviewerStatusUpdate.java
index 938d985..1e0aa43 100644
--- a/java/com/google/gerrit/server/ReviewerStatusUpdate.java
+++ b/java/com/google/gerrit/server/ReviewerStatusUpdate.java
@@ -17,17 +17,17 @@
import com.google.auto.value.AutoValue;
import com.google.gerrit.entities.Account;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
-import java.sql.Timestamp;
+import java.time.Instant;
/** Change to a reviewer's status. */
@AutoValue
public abstract class ReviewerStatusUpdate {
public static ReviewerStatusUpdate create(
- Timestamp ts, Account.Id updatedBy, Account.Id reviewer, ReviewerStateInternal state) {
+ Instant ts, Account.Id updatedBy, Account.Id reviewer, ReviewerStateInternal state) {
return new AutoValue_ReviewerStatusUpdate(ts, updatedBy, reviewer, state);
}
- public abstract Timestamp date();
+ public abstract Instant date();
public abstract Account.Id updatedBy();
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index 5dcbd01..33f2ad1 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -54,10 +54,10 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Collection;
-import java.util.HashSet;
+import java.util.Collections;
import java.util.List;
+import java.util.NavigableSet;
import java.util.Set;
-import java.util.SortedSet;
import java.util.TreeSet;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.Constants;
@@ -113,7 +113,7 @@
@AutoValue
public abstract static class StarRef {
private static final StarRef MISSING =
- new AutoValue_StarredChangesUtil_StarRef(null, ImmutableSortedSet.of());
+ new AutoValue_StarredChangesUtil_StarRef(null, Collections.emptyNavigableSet());
private static StarRef create(Ref ref, Iterable<String> labels) {
return new AutoValue_StarredChangesUtil_StarRef(
@@ -123,7 +123,7 @@
@Nullable
public abstract Ref ref();
- public abstract ImmutableSortedSet<String> labels();
+ public abstract NavigableSet<String> labels();
public ObjectId objectId() {
return ref() != null ? ref().getObjectId() : ObjectId.zeroId();
@@ -185,7 +185,7 @@
this.queryProvider = queryProvider;
}
- public ImmutableSortedSet<String> getLabels(Account.Id accountId, Change.Id changeId) {
+ public NavigableSet<String> getLabels(Account.Id accountId, Change.Id changeId) {
try (Repository repo = repoManager.openRepository(allUsers)) {
return readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)).labels();
} catch (IOException e) {
@@ -197,7 +197,7 @@
}
}
- public ImmutableSortedSet<String> star(
+ public NavigableSet<String> star(
Account.Id accountId,
Project.NameKey project,
Change.Id changeId,
@@ -208,7 +208,7 @@
String refName = RefNames.refsStarredChanges(changeId, accountId);
StarRef old = readLabels(repo, refName);
- Set<String> labels = new HashSet<>(old.labels());
+ NavigableSet<String> labels = new TreeSet<>(old.labels());
if (labelsToAdd != null) {
labels.addAll(labelsToAdd);
}
@@ -224,7 +224,7 @@
}
indexer.index(project, changeId);
- return ImmutableSortedSet.copyOf(labels);
+ return Collections.unmodifiableNavigableSet(labels);
} catch (IOException e) {
throw new StorageException(
String.format("Star change %d for account %d failed", changeId.get(), accountId.get()),
@@ -289,6 +289,35 @@
}
}
+ public ImmutableSet<Change.Id> byAccountId(Account.Id accountId, String label) {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ ImmutableSet.Builder<Change.Id> builder = ImmutableSet.builder();
+ for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_STARRED_CHANGES)) {
+ Account.Id currentAccountId = Account.Id.fromRef(ref.getName());
+ // Skip all refs that don't correspond with accountId.
+ if (currentAccountId == null || !currentAccountId.equals(accountId)) {
+ continue;
+ }
+ // Skip all refs that don't contain the required label.
+ StarRef starRef = readLabels(repo, ref.getName());
+ if (!starRef.labels().contains(label)) {
+ continue;
+ }
+
+ // Skip invalid change ids.
+ Change.Id changeId = Change.Id.fromAllUsersRef(ref.getName());
+ if (changeId == null) {
+ continue;
+ }
+ builder.add(changeId);
+ }
+ return builder.build();
+ } catch (IOException e) {
+ throw new StorageException(
+ String.format("Get starred changes for account %d failed", accountId.get()), e);
+ }
+ }
+
public ImmutableListMultimap<Account.Id, String> byChangeFromIndex(Change.Id changeId) {
List<ChangeData> changeData =
queryProvider
@@ -391,7 +420,7 @@
return;
}
- SortedSet<String> invalidLabels = new TreeSet<>();
+ NavigableSet<String> invalidLabels = new TreeSet<>();
for (String label : labels) {
if (CharMatcher.whitespace().matchesAnyOf(label)) {
invalidLabels.add(label);
diff --git a/java/com/google/gerrit/server/WebLinks.java b/java/com/google/gerrit/server/WebLinks.java
index 3c69573..3a82694 100644
--- a/java/com/google/gerrit/server/WebLinks.java
+++ b/java/com/google/gerrit/server/WebLinks.java
@@ -39,23 +39,11 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.function.Function;
-import java.util.function.Predicate;
@Singleton
public class WebLinks {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- private static final Predicate<WebLinkInfo> INVALID_WEBLINK =
- link -> {
- if (link == null) {
- return false;
- } else if (Strings.isNullOrEmpty(link.name) || Strings.isNullOrEmpty(link.url)) {
- logger.atWarning().log("%s is missing name and/or url", link.getClass().getName());
- return false;
- }
- return true;
- };
-
private final DynamicSet<PatchSetWebLink> patchSetLinks;
private final DynamicSet<ResolveConflictsWebLink> resolveConflictsLinks;
private final DynamicSet<ParentWebLink> parentLinks;
@@ -177,7 +165,7 @@
}
return Streams.stream(fileHistoryLinks)
.map(webLink -> webLink.getFileHistoryWebLink(project, revision, file))
- .filter(INVALID_WEBLINK)
+ .filter(WebLinks::isValid)
.collect(toImmutableList());
}
@@ -216,7 +204,7 @@
patchSetIdB,
revisionB,
fileB))
- .filter(INVALID_WEBLINK)
+ .filter(WebLinks::isValid)
.collect(toImmutableList());
}
@@ -253,7 +241,17 @@
DynamicSet<T> links, Function<T, WebLinkInfo> transformer) {
return Streams.stream(links)
.map(transformer)
- .filter(INVALID_WEBLINK)
+ .filter(WebLinks::isValid)
.collect(toImmutableList());
}
+
+ private static boolean isValid(WebLinkInfo link) {
+ if (link == null) {
+ return false;
+ } else if (Strings.isNullOrEmpty(link.name) || Strings.isNullOrEmpty(link.url)) {
+ logger.atWarning().log("%s is missing name and/or url", link.getClass().getName());
+ return false;
+ }
+ return true;
+ }
}
diff --git a/java/com/google/gerrit/server/account/AccountCacheImpl.java b/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 093af68..1d9150d 100644
--- a/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -153,7 +153,7 @@
}
private AccountState missing(Account.Id accountId) {
- Account.Builder account = Account.builder(accountId, TimeUtil.nowTs());
+ Account.Builder account = Account.builder(accountId, TimeUtil.now());
account.setActive(false);
return AccountState.forAccount(account.build());
}
diff --git a/java/com/google/gerrit/server/account/AccountConfig.java b/java/com/google/gerrit/server/account/AccountConfig.java
index 45f1f35..28e881e1 100644
--- a/java/com/google/gerrit/server/account/AccountConfig.java
+++ b/java/com/google/gerrit/server/account/AccountConfig.java
@@ -34,8 +34,9 @@
import com.google.gerrit.server.git.meta.VersionedMetaData;
import com.google.gerrit.server.util.time.TimeUtil;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.ArrayList;
+import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -177,7 +178,7 @@
* @throws DuplicateKeyException if the user branch already exists
*/
public Account getNewAccount() throws DuplicateKeyException {
- return getNewAccount(TimeUtil.nowTs());
+ return getNewAccount(TimeUtil.now());
}
/**
@@ -186,7 +187,7 @@
* @return the new account
* @throws DuplicateKeyException if the user branch already exists
*/
- Account getNewAccount(Timestamp registeredOn) throws DuplicateKeyException {
+ Account getNewAccount(Instant registeredOn) throws DuplicateKeyException {
checkLoaded();
if (revision != null) {
throw new DuplicateKeyException(String.format("account %s already exists", accountId));
@@ -216,7 +217,7 @@
rw.reset();
rw.markStart(revision);
rw.sort(RevSort.REVERSE);
- Timestamp registeredOn = new Timestamp(rw.next().getCommitTime() * 1000L);
+ Instant registeredOn = Instant.ofEpochMilli(rw.next().getCommitTime() * 1000L);
Config accountConfig = readConfig(AccountProperties.ACCOUNT_CONFIG);
loadedAccountProperties =
@@ -257,6 +258,9 @@
return c;
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Override
protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
checkLoaded();
@@ -274,9 +278,9 @@
commit.setMessage("Create account\n");
}
- Timestamp registeredOn = loadedAccountProperties.get().getRegisteredOn();
- commit.setAuthor(new PersonIdent(commit.getAuthor(), registeredOn));
- commit.setCommitter(new PersonIdent(commit.getCommitter(), registeredOn));
+ Instant registeredOn = loadedAccountProperties.get().getRegisteredOn();
+ commit.setAuthor(new PersonIdent(commit.getAuthor(), Date.from(registeredOn)));
+ commit.setCommitter(new PersonIdent(commit.getCommitter(), Date.from(registeredOn)));
}
saveAccount();
diff --git a/java/com/google/gerrit/server/account/AccountProperties.java b/java/com/google/gerrit/server/account/AccountProperties.java
index 9b7ca81..928d851 100644
--- a/java/com/google/gerrit/server/account/AccountProperties.java
+++ b/java/com/google/gerrit/server/account/AccountProperties.java
@@ -17,7 +17,7 @@
import com.google.common.base.Strings;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
-import java.sql.Timestamp;
+import java.time.Instant;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
@@ -57,16 +57,13 @@
public static final String KEY_STATUS = "status";
private final Account.Id accountId;
- private final Timestamp registeredOn;
+ private final Instant registeredOn;
private final Config accountConfig;
private @Nullable ObjectId metaId;
private Account account;
AccountProperties(
- Account.Id accountId,
- Timestamp registeredOn,
- Config accountConfig,
- @Nullable ObjectId metaId) {
+ Account.Id accountId, Instant registeredOn, Config accountConfig, @Nullable ObjectId metaId) {
this.accountId = accountId;
this.registeredOn = registeredOn;
this.accountConfig = accountConfig;
@@ -80,7 +77,7 @@
return account;
}
- public Timestamp getRegisteredOn() {
+ public Instant getRegisteredOn() {
return registeredOn;
}
diff --git a/java/com/google/gerrit/server/account/AccountResolver.java b/java/com/google/gerrit/server/account/AccountResolver.java
index 68f5a85..1eb65ec 100644
--- a/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/java/com/google/gerrit/server/account/AccountResolver.java
@@ -538,12 +538,12 @@
* @throws IOException if an error occurs.
*/
public Result resolve(String input) throws ConfigInvalidException, IOException {
- return searchImpl(input, searchers, visibilitySupplierCanSee(), accountActivityPredicate());
+ return searchImpl(input, searchers, this::canSeePredicate, AccountResolver::isActive);
}
public Result resolve(String input, Predicate<AccountState> accountActivityPredicate)
throws ConfigInvalidException, IOException {
- return searchImpl(input, searchers, visibilitySupplierCanSee(), accountActivityPredicate);
+ return searchImpl(input, searchers, this::canSeePredicate, accountActivityPredicate);
}
/**
@@ -556,17 +556,17 @@
* instead will be stored as a link to the corresponding Gerrit Account.
*/
public Result resolveIncludeInactive(String input) throws ConfigInvalidException, IOException {
- return searchImpl(input, searchers, visibilitySupplierCanSee(), all());
+ return searchImpl(input, searchers, this::canSeePredicate, AccountResolver::allVisible);
}
public Result resolveIgnoreVisibility(String input) throws ConfigInvalidException, IOException {
- return searchImpl(input, searchers, visibilitySupplierAll(), accountActivityPredicate());
+ return searchImpl(input, searchers, this::allVisiblePredicate, AccountResolver::isActive);
}
public Result resolveIgnoreVisibility(
String input, Predicate<AccountState> accountActivityPredicate)
throws ConfigInvalidException, IOException {
- return searchImpl(input, searchers, visibilitySupplierAll(), accountActivityPredicate);
+ return searchImpl(input, searchers, this::allVisiblePredicate, accountActivityPredicate);
}
/**
@@ -595,7 +595,7 @@
@Deprecated
public Result resolveByNameOrEmail(String input) throws ConfigInvalidException, IOException {
return searchImpl(
- input, nameOrEmailSearchers, visibilitySupplierCanSee(), accountActivityPredicate());
+ input, nameOrEmailSearchers, this::canSeePredicate, AccountResolver::isActive);
}
/**
@@ -614,26 +614,29 @@
return searchImpl(
input,
ImmutableList.of(new ByNameAndEmail(), new ByEmail(), new ByFullName(), new ByUsername()),
- visibilitySupplierCanSee(),
- accountActivityPredicate());
+ this::canSeePredicate,
+ AccountResolver::isActive);
}
- private Supplier<Predicate<AccountState>> visibilitySupplierCanSee() {
- return () -> accountControlFactory.get()::canSee;
+ private Predicate<AccountState> canSeePredicate() {
+ return this::canSee;
}
- private Supplier<Predicate<AccountState>> visibilitySupplierAll() {
- return () -> all();
+ private boolean canSee(AccountState accountState) {
+ return accountControlFactory.get().canSee(accountState);
}
- private Predicate<AccountState> all() {
- return accountState -> {
- return true;
- };
+ private Predicate<AccountState> allVisiblePredicate() {
+ return AccountResolver::allVisible;
}
- private Predicate<AccountState> accountActivityPredicate() {
- return (AccountState accountState) -> accountState.account().isActive();
+ /** @param accountState account state for which the visibility should be checked */
+ private static boolean allVisible(AccountState accountState) {
+ return true;
+ }
+
+ private static boolean isActive(AccountState accountState) {
+ return accountState.account().isActive();
}
@VisibleForTesting
diff --git a/java/com/google/gerrit/server/account/AccountResource.java b/java/com/google/gerrit/server/account/AccountResource.java
index 4fb69bd..9629809 100644
--- a/java/com/google/gerrit/server/account/AccountResource.java
+++ b/java/com/google/gerrit/server/account/AccountResource.java
@@ -23,20 +23,16 @@
import java.util.Set;
public class AccountResource implements RestResource {
- public static final TypeLiteral<RestView<AccountResource>> ACCOUNT_KIND =
- new TypeLiteral<RestView<AccountResource>>() {};
+ public static final TypeLiteral<RestView<AccountResource>> ACCOUNT_KIND = new TypeLiteral<>() {};
- public static final TypeLiteral<RestView<Capability>> CAPABILITY_KIND =
- new TypeLiteral<RestView<Capability>>() {};
+ public static final TypeLiteral<RestView<Capability>> CAPABILITY_KIND = new TypeLiteral<>() {};
- public static final TypeLiteral<RestView<Email>> EMAIL_KIND =
- new TypeLiteral<RestView<Email>>() {};
+ public static final TypeLiteral<RestView<Email>> EMAIL_KIND = new TypeLiteral<>() {};
- public static final TypeLiteral<RestView<SshKey>> SSH_KEY_KIND =
- new TypeLiteral<RestView<SshKey>>() {};
+ public static final TypeLiteral<RestView<SshKey>> SSH_KEY_KIND = new TypeLiteral<>() {};
public static final TypeLiteral<RestView<StarredChange>> STARRED_CHANGE_KIND =
- new TypeLiteral<RestView<StarredChange>>() {};
+ new TypeLiteral<>() {};
private final IdentifiedUser user;
@@ -106,8 +102,7 @@
}
public static class Star implements RestResource {
- public static final TypeLiteral<RestView<Star>> STAR_KIND =
- new TypeLiteral<RestView<Star>>() {};
+ public static final TypeLiteral<RestView<Star>> STAR_KIND = new TypeLiteral<>() {};
private final IdentifiedUser user;
private final ChangeResource change;
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index 93738b0..3ee6365 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -49,7 +49,6 @@
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
-import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -201,8 +200,6 @@
/** Single instance that accumulates updates from the batch. */
private ExternalIdNotes externalIdNotes;
- private static final Runnable DO_NOTHING = () -> {};
-
@AssistedInject
@SuppressWarnings("BindingAnnotationWithoutInject")
AccountsUpdate(
@@ -225,8 +222,8 @@
extIdNotesLoader,
serverIdent,
createPersonIdent(serverIdent, Optional.empty()),
- DO_NOTHING,
- DO_NOTHING);
+ AccountsUpdate::doNothing,
+ AccountsUpdate::doNothing);
}
@AssistedInject
@@ -252,8 +249,8 @@
extIdNotesLoader,
serverIdent,
createPersonIdent(serverIdent, Optional.of(currentUser)),
- DO_NOTHING,
- DO_NOTHING);
+ AccountsUpdate::doNothing,
+ AccountsUpdate::doNothing);
}
@VisibleForTesting
@@ -297,9 +294,7 @@
private static PersonIdent createPersonIdent(
PersonIdent serverIdent, Optional<IdentifiedUser> user) {
- return user.isPresent()
- ? user.get().newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone())
- : serverIdent;
+ return user.isPresent() ? user.get().newCommitterIdent(serverIdent) : serverIdent;
}
/**
@@ -324,6 +319,9 @@
* @throws IOException if creating the user branch fails due to an IO error
* @throws ConfigInvalidException if any of the account fields has an invalid value
*/
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
public AccountState insert(String message, Account.Id accountId, ConfigureDeltaFromState init)
throws IOException, ConfigInvalidException {
return execute(
@@ -331,8 +329,7 @@
repo -> {
AccountConfig accountConfig = read(repo, accountId);
Account account =
- accountConfig.getNewAccount(
- new Timestamp(committerIdent.getWhen().getTime()));
+ accountConfig.getNewAccount(committerIdent.getWhen().toInstant());
AccountState accountState = AccountState.forAccount(account);
AccountDelta.Builder deltaBuilder = AccountDelta.builder();
init.configure(accountState, deltaBuilder);
@@ -586,6 +583,8 @@
return metaDataUpdate;
}
+ private static void doNothing() {}
+
@FunctionalInterface
private interface ExecutableUpdate {
UpdatedAccount execute(Repository allUsersRepo) throws IOException, ConfigInvalidException;
diff --git a/java/com/google/gerrit/server/account/AuthRequest.java b/java/com/google/gerrit/server/account/AuthRequest.java
index 50ed532..cceda70 100644
--- a/java/com/google/gerrit/server/account/AuthRequest.java
+++ b/java/com/google/gerrit/server/account/AuthRequest.java
@@ -49,20 +49,20 @@
}
/** Create a request for a local username, such as from LDAP. */
- public AuthRequest createForUser(String username) {
+ public AuthRequest createForUser(String userName) {
AuthRequest r =
new AuthRequest(
- externalIdKeyFactory.create(SCHEME_GERRIT, username), externalIdKeyFactory);
- r.setUserName(username);
+ externalIdKeyFactory.create(SCHEME_GERRIT, userName), externalIdKeyFactory);
+ r.setUserName(userName);
return r;
}
/** Create a request for an external username. */
- public AuthRequest createForExternalUser(String username) {
+ public AuthRequest createForExternalUser(String userName) {
AuthRequest r =
new AuthRequest(
- externalIdKeyFactory.create(SCHEME_EXTERNAL, username), externalIdKeyFactory);
- r.setUserName(username);
+ externalIdKeyFactory.create(SCHEME_EXTERNAL, userName), externalIdKeyFactory);
+ r.setUserName(userName);
return r;
}
diff --git a/java/com/google/gerrit/server/account/CachedAccountDetails.java b/java/com/google/gerrit/server/account/CachedAccountDetails.java
index f23a766..2ab6174 100644
--- a/java/com/google/gerrit/server/account/CachedAccountDetails.java
+++ b/java/com/google/gerrit/server/account/CachedAccountDetails.java
@@ -30,7 +30,6 @@
import com.google.gerrit.server.cache.serialize.CacheSerializer;
import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
import com.google.gerrit.server.config.CachedPreferences;
-import java.sql.Timestamp;
import java.time.Instant;
import java.util.Map;
import org.eclipse.jgit.lib.ObjectId;
@@ -107,7 +106,7 @@
Cache.AccountProto.Builder accountProto =
Cache.AccountProto.newBuilder()
.setId(account.id().get())
- .setRegisteredOn(account.registeredOn().toInstant().toEpochMilli())
+ .setRegisteredOn(account.registeredOn().toEpochMilli())
.setInactive(account.inactive())
.setFullName(Strings.nullToEmpty(account.fullName()))
.setDisplayName(Strings.nullToEmpty(account.displayName()))
@@ -143,7 +142,7 @@
Account account =
Account.builder(
Account.id(proto.getAccount().getId()),
- Timestamp.from(Instant.ofEpochMilli(proto.getAccount().getRegisteredOn())))
+ Instant.ofEpochMilli(proto.getAccount().getRegisteredOn()))
.setFullName(Strings.emptyToNull(proto.getAccount().getFullName()))
.setDisplayName(Strings.emptyToNull(proto.getAccount().getDisplayName()))
.setPreferredEmail(Strings.emptyToNull(proto.getAccount().getPreferredEmail()))
diff --git a/java/com/google/gerrit/server/account/Emails.java b/java/com/google/gerrit/server/account/Emails.java
index 98d0d50..45f0844 100644
--- a/java/com/google/gerrit/server/account/Emails.java
+++ b/java/com/google/gerrit/server/account/Emails.java
@@ -29,7 +29,6 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
@@ -114,11 +113,14 @@
return externalIds.byEmail(email).stream().map(ExternalId::accountId).collect(toImmutableSet());
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
public UserIdentity toUserIdentity(PersonIdent who) throws IOException {
UserIdentity u = new UserIdentity();
u.setName(who.getName());
u.setEmail(who.getEmailAddress());
- u.setDate(new Timestamp(who.getWhen().getTime()));
+ u.setDate(who.getWhen().toInstant());
u.setTimeZone(who.getTimeZoneOffset());
// If only one account has access to this email address, select it
diff --git a/java/com/google/gerrit/server/account/Realm.java b/java/com/google/gerrit/server/account/Realm.java
index 3f642f7..ffc95a3 100644
--- a/java/com/google/gerrit/server/account/Realm.java
+++ b/java/com/google/gerrit/server/account/Realm.java
@@ -56,7 +56,14 @@
*/
Account.Id lookup(String accountName) throws IOException;
- /** Returns true if the account is active. */
+ /**
+ * Returns true if the account is active.
+ *
+ * @throws LoginException thrown if login is required and fails
+ * @throws NamingException may be thrown if the name is invalid
+ * @throws AccountException may be thrown in case the username is ambiguous
+ * @throws IOException thrown in case of IO errors
+ */
default boolean isActive(@SuppressWarnings("unused") String username)
throws LoginException, NamingException, AccountException, IOException {
return true;
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalId.java b/java/com/google/gerrit/server/account/externalids/ExternalId.java
index d27ff0e..a5fb733 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -308,7 +308,7 @@
ExternalId o = (ExternalId) obj;
return Objects.equals(key(), o.key())
&& Objects.equals(accountId(), o.accountId())
- && Objects.equals(isCaseInsensitive(), o.isCaseInsensitive())
+ && isCaseInsensitive() == o.isCaseInsensitive()
&& Objects.equals(email(), o.email())
&& Objects.equals(password(), o.password());
}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java b/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
index bd9c7df..b16f73f 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
@@ -19,7 +19,6 @@
import com.google.common.base.Strings;
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.server.account.HashedPassword;
@@ -33,7 +32,6 @@
@Singleton
public class ExternalIdFactory {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final ExternalIdKeyFactory externalIdKeyFactory;
private AuthConfig authConfig;
@@ -264,7 +262,8 @@
throw invalidConfig(
noteId,
String.format(
- "Neither case sensitive nor case insensitive SHA1 of external ID '%s' match note ID '%s'",
+ "Neither case sensitive nor case insensitive SHA1 of external ID '%s' match note ID"
+ + " '%s'",
externalIdKeyStr, noteId));
}
externalIdKey =
@@ -316,15 +315,17 @@
}
return accountId;
} catch (IllegalArgumentException e) {
- String msg =
- String.format(
- "Value %s for '%s.%s.%s' is invalid, expected account ID",
- accountIdStr,
- ExternalId.EXTERNAL_ID_SECTION,
- externalIdKeyStr,
- ExternalId.ACCOUNT_ID_KEY);
- logger.atSevere().withCause(e).log(msg);
- throw invalidConfig(noteId, msg);
+ ConfigInvalidException newException =
+ invalidConfig(
+ noteId,
+ String.format(
+ "Value %s for '%s.%s.%s' is invalid, expected account ID",
+ accountIdStr,
+ ExternalId.EXTERNAL_ID_SECTION,
+ externalIdKeyStr,
+ ExternalId.ACCOUNT_ID_KEY));
+ newException.initCause(e);
+ throw newException;
}
}
diff --git a/java/com/google/gerrit/server/account/externalids/OnlineExternalIdCaseSensivityMigrator.java b/java/com/google/gerrit/server/account/externalids/OnlineExternalIdCaseSensivityMigrator.java
index e52991b..72e7e90 100644
--- a/java/com/google/gerrit/server/account/externalids/OnlineExternalIdCaseSensivityMigrator.java
+++ b/java/com/google/gerrit/server/account/externalids/OnlineExternalIdCaseSensivityMigrator.java
@@ -70,7 +70,9 @@
public void migrate() {
if (!isUserNameCaseInsensitive || !isUserNameCaseInsensitiveMigrationMode) {
logger.atSevere().log(
- "External IDs online migration requires auth.userNameCaseInsensitive and auth.userNameCaseInsensitiveMigrationMode to be set to true. Skipping migration!");
+ "External IDs online migration requires auth.userNameCaseInsensitive and"
+ + " auth.userNameCaseInsensitiveMigrationMode to be set to true. Skipping"
+ + " migration!");
return;
}
executor.execute(
diff --git a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index 764c46d..9aa9306 100644
--- a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -86,7 +86,6 @@
import com.google.gerrit.server.restapi.change.ListRobotComments;
import com.google.gerrit.server.restapi.change.Mergeable;
import com.google.gerrit.server.restapi.change.PostReview;
-import com.google.gerrit.server.restapi.change.PreviewSubmit;
import com.google.gerrit.server.restapi.change.PutDescription;
import com.google.gerrit.server.restapi.change.Rebase;
import com.google.gerrit.server.restapi.change.Reviewed;
@@ -119,7 +118,6 @@
private final Rebase rebase;
private final RebaseUtil rebaseUtil;
private final Submit submit;
- private final PreviewSubmit submitPreview;
private final Reviewed.PutReviewed putReviewed;
private final Reviewed.DeleteReviewed deleteReviewed;
private final RevisionResource revision;
@@ -167,7 +165,6 @@
Rebase rebase,
RebaseUtil rebaseUtil,
Submit submit,
- PreviewSubmit submitPreview,
Reviewed.PutReviewed putReviewed,
Reviewed.DeleteReviewed deleteReviewed,
Files files,
@@ -213,7 +210,6 @@
this.rebaseUtil = rebaseUtil;
this.review = review;
this.submit = submit;
- this.submitPreview = submitPreview;
this.files = files;
this.putReviewed = putReviewed;
this.deleteReviewed = deleteReviewed;
@@ -270,16 +266,6 @@
}
@Override
- public BinaryResult submitPreview(String format) throws RestApiException {
- try {
- submitPreview.setFormat(format);
- return submitPreview.apply(revision).value();
- } catch (Exception e) {
- throw asRestApiException("Cannot get submit preview", e);
- }
- }
-
- @Override
public ChangeApi rebase(RebaseInput in) throws RestApiException {
try {
return changes.id(rebaseAsInfo(in)._number);
diff --git a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index 9521759..5baed86 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -507,7 +507,7 @@
@Override
public ListRefsRequest<BranchInfo> branches() {
- return new ListRefsRequest<BranchInfo>() {
+ return new ListRefsRequest<>() {
@Override
public List<BranchInfo> get() throws RestApiException {
try {
@@ -521,7 +521,7 @@
@Override
public ListRefsRequest<TagInfo> tags() {
- return new ListRefsRequest<TagInfo>() {
+ return new ListRefsRequest<>() {
@Override
public List<TagInfo> get() throws RestApiException {
try {
diff --git a/java/com/google/gerrit/server/approval/ApprovalCache.java b/java/com/google/gerrit/server/approval/ApprovalCache.java
deleted file mode 100644
index 5637249..0000000
--- a/java/com/google/gerrit/server/approval/ApprovalCache.java
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.approval;
-
-import com.google.gerrit.entities.PatchSet;
-import com.google.gerrit.entities.PatchSetApproval;
-import com.google.gerrit.server.notedb.ChangeNotes;
-
-/**
- * Cache that holds approvals per patch set and NoteDb state. This includes approvals copied forward
- * from older patch sets.
- */
-public interface ApprovalCache {
- /** Returns {@link PatchSetApproval}s for the given patch set. */
- Iterable<PatchSetApproval> get(ChangeNotes notes, PatchSet.Id psId);
-}
diff --git a/java/com/google/gerrit/server/approval/ApprovalCacheImpl.java b/java/com/google/gerrit/server/approval/ApprovalCacheImpl.java
deleted file mode 100644
index fd31da9..0000000
--- a/java/com/google/gerrit/server/approval/ApprovalCacheImpl.java
+++ /dev/null
@@ -1,133 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.approval;
-
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.PatchSet;
-import com.google.gerrit.entities.PatchSetApproval;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.converter.PatchSetApprovalProtoConverter;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.proto.Entities;
-import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.proto.Cache;
-import com.google.gerrit.server.cache.serialize.ObjectIdCacheSerializer;
-import com.google.gerrit.server.cache.serialize.ProtobufSerializer;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.inject.Inject;
-import com.google.inject.Module;
-import com.google.inject.Singleton;
-import com.google.inject.name.Named;
-import com.google.protobuf.ByteString;
-import java.util.concurrent.ExecutionException;
-
-/** Implementation of the {@link ApprovalCache} interface */
-public class ApprovalCacheImpl implements ApprovalCache {
- private static final String CACHE_NAME = "approvals";
-
- public static Module module() {
- return new CacheModule() {
- @Override
- protected void configure() {
- bind(ApprovalCache.class).to(ApprovalCacheImpl.class);
- persist(
- CACHE_NAME,
- Cache.PatchSetApprovalsKeyProto.class,
- Cache.AllPatchSetApprovalsProto.class)
- .version(2)
- .loader(Loader.class)
- .keySerializer(new ProtobufSerializer<>(Cache.PatchSetApprovalsKeyProto.parser()))
- .valueSerializer(new ProtobufSerializer<>(Cache.AllPatchSetApprovalsProto.parser()));
- }
- };
- }
-
- private final LoadingCache<Cache.PatchSetApprovalsKeyProto, Cache.AllPatchSetApprovalsProto>
- cache;
-
- @Inject
- ApprovalCacheImpl(
- @Named(CACHE_NAME)
- LoadingCache<Cache.PatchSetApprovalsKeyProto, Cache.AllPatchSetApprovalsProto> cache) {
- this.cache = cache;
- }
-
- @Override
- public Iterable<PatchSetApproval> get(ChangeNotes notes, PatchSet.Id psId) {
- try {
- return fromProto(
- cache.get(
- Cache.PatchSetApprovalsKeyProto.newBuilder()
- .setChangeId(notes.getChangeId().get())
- .setPatchSetId(psId.get())
- .setProject(notes.getProjectName().get())
- .setId(
- ByteString.copyFrom(
- ObjectIdCacheSerializer.INSTANCE.serialize(notes.getMetaId())))
- .build()));
- } catch (ExecutionException e) {
- throw new StorageException(e);
- }
- }
-
- @Singleton
- static class Loader
- extends CacheLoader<Cache.PatchSetApprovalsKeyProto, Cache.AllPatchSetApprovalsProto> {
- private final ApprovalInference approvalInference;
- private final ChangeNotes.Factory changeNotesFactory;
-
- @Inject
- Loader(ApprovalInference approvalInference, ChangeNotes.Factory changeNotesFactory) {
- this.approvalInference = approvalInference;
- this.changeNotesFactory = changeNotesFactory;
- }
-
- @Override
- public Cache.AllPatchSetApprovalsProto load(Cache.PatchSetApprovalsKeyProto key)
- throws Exception {
- Change.Id changeId = Change.id(key.getChangeId());
- return toProto(
- approvalInference.forPatchSet(
- changeNotesFactory.createChecked(
- Project.nameKey(key.getProject()),
- changeId,
- ObjectIdCacheSerializer.INSTANCE.deserialize(key.getId().toByteArray())),
- PatchSet.id(changeId, key.getPatchSetId()),
- null
- /* revWalk= */ ,
- null
- /* repoConfig= */ ));
- }
- }
-
- private static Iterable<PatchSetApproval> fromProto(Cache.AllPatchSetApprovalsProto proto) {
- ImmutableList.Builder<PatchSetApproval> builder = ImmutableList.builder();
- for (Entities.PatchSetApproval psa : proto.getApprovalList()) {
- builder.add(PatchSetApprovalProtoConverter.INSTANCE.fromProto(psa));
- }
- return builder.build();
- }
-
- private static Cache.AllPatchSetApprovalsProto toProto(Iterable<PatchSetApproval> autoValue) {
- Cache.AllPatchSetApprovalsProto.Builder builder = Cache.AllPatchSetApprovalsProto.newBuilder();
- for (PatchSetApproval psa : autoValue) {
- builder.addApproval(PatchSetApprovalProtoConverter.INSTANCE.toProto(psa));
- }
- return builder.build();
- }
-}
diff --git a/java/com/google/gerrit/server/approval/ApprovalInference.java b/java/com/google/gerrit/server/approval/ApprovalInference.java
index 695997a..fba2fcb 100644
--- a/java/com/google/gerrit/server/approval/ApprovalInference.java
+++ b/java/com/google/gerrit/server/approval/ApprovalInference.java
@@ -39,7 +39,8 @@
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.patch.DiffNotAvailableException;
import com.google.gerrit.server.patch.DiffOperations;
-import com.google.gerrit.server.patch.filediff.FileDiffOutput;
+import com.google.gerrit.server.patch.DiffOptions;
+import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.approval.ApprovalContext;
@@ -50,7 +51,6 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.Collection;
-import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jgit.lib.Config;
@@ -97,20 +97,11 @@
}
/**
- * Returns all approvals that apply to the given patch set. Honors direct and indirect (approval
- * on parents) approvals.
+ * Returns all approvals that apply to the given patch set. Honors copied approvals from previous
+ * patch-set.
*/
Iterable<PatchSetApproval> forPatchSet(
- ChangeNotes notes, PatchSet.Id psId, @Nullable RevWalk rw, @Nullable Config repoConfig) {
- PatchSet patchset = notes.getPatchSets().get(psId);
- if (patchset == null) {
- return Collections.emptyList();
- }
- return forPatchSet(notes, patchset, rw, repoConfig);
- }
-
- Iterable<PatchSetApproval> forPatchSet(
- ChangeNotes notes, PatchSet ps, @Nullable RevWalk rw, @Nullable Config repoConfig) {
+ ChangeNotes notes, PatchSet ps, RevWalk rw, Config repoConfig) {
ProjectState project;
try (TraceTimer traceTimer =
TraceContext.newTimer(
@@ -135,9 +126,9 @@
PatchSet.Id psId,
ChangeKind kind,
LabelType type,
- @Nullable Map<String, FileDiffOutput> baseVsCurrentDiff,
- @Nullable Map<String, FileDiffOutput> baseVsPriorDiff,
- @Nullable Map<String, FileDiffOutput> priorVsCurrentDiff) {
+ @Nullable Map<String, ModifiedFile> baseVsCurrentDiff,
+ @Nullable Map<String, ModifiedFile> baseVsPriorDiff,
+ @Nullable Map<String, ModifiedFile> priorVsCurrentDiff) {
int n = psa.key().patchSetId().get();
checkArgument(n != psId.get());
@@ -325,11 +316,14 @@
PatchSetApproval psa,
PatchSet patchSet,
LabelType type,
- ChangeKind changeKind) {
+ ChangeKind changeKind,
+ RevWalk revWalk,
+ Config repoConfig) {
if (!type.getCopyCondition().isPresent()) {
return false;
}
- ApprovalContext ctx = ApprovalContext.create(changeNotes, psa, patchSet, changeKind);
+ ApprovalContext ctx =
+ ApprovalContext.create(changeNotes, psa, patchSet, changeKind, revWalk, repoConfig);
try {
// Use a request context to run checks as an internal user with expanded visibility. This is
// so that the output of the copy condition does not depend on who is running the current
@@ -346,11 +340,7 @@
}
private Collection<PatchSetApproval> getForPatchSetWithoutNormalization(
- ChangeNotes notes,
- ProjectState project,
- PatchSet patchSet,
- @Nullable RevWalk rw,
- @Nullable Config repoConfig) {
+ ChangeNotes notes, ProjectState project, PatchSet patchSet, RevWalk rw, Config repoConfig) {
checkState(
project.getNameKey().equals(notes.getProjectName()),
"project must match %s, %s",
@@ -360,34 +350,23 @@
PatchSet.Id psId = patchSet.id();
// Add approvals on the given patch set to the result
Table<String, Account.Id, PatchSetApproval> resultByUser = HashBasedTable.create();
- ImmutableList<PatchSetApproval> approvalsForGivenPatchSet =
+ ImmutableList<PatchSetApproval> nonCopiedApprovalsForGivenPatchSet =
notes.load().getApprovals().get(patchSet.id());
- approvalsForGivenPatchSet.forEach(psa -> resultByUser.put(psa.label(), psa.accountId(), psa));
+ nonCopiedApprovalsForGivenPatchSet.forEach(
+ psa -> resultByUser.put(psa.label(), psa.accountId(), psa));
// Bail out immediately if this is the first patch set. Return only approvals granted on the
// given patch set.
if (psId.get() == 1) {
return resultByUser.values();
}
-
- // Call this algorithm recursively to check if the prior patch set had approvals. This has the
- // advantage that all caches - most importantly ChangeKindCache - have values cached for what we
- // need for this computation.
- // The way this algorithm is written is that any approval will be copied forward by one patch
- // set at a time if configs and change kind allow so. Once an approval is held back - for
- // example because the patch set is a REWORK - it will not be picked up again in a future
- // patch set.
Map.Entry<PatchSet.Id, PatchSet> priorPatchSet = notes.load().getPatchSets().lowerEntry(psId);
if (priorPatchSet == null) {
return resultByUser.values();
}
- Iterable<PatchSetApproval> priorApprovals =
- getForPatchSetWithoutNormalization(
- notes, project, priorPatchSet.getValue(), rw, repoConfig);
- if (!priorApprovals.iterator().hasNext()) {
- return resultByUser.values();
- }
+ ImmutableList<PatchSetApproval> priorApprovalsIncludingCopied =
+ notes.load().getApprovalsWithCopied().get(priorPatchSet.getKey());
// Add labels from the previous patch set to the result in case the label isn't already there
// and settings as well as change kind allow copying.
@@ -405,11 +384,11 @@
priorPatchSet.getValue().id().changeId(),
changeKind);
- Map<String, FileDiffOutput> baseVsCurrent = null;
- Map<String, FileDiffOutput> baseVsPrior = null;
- Map<String, FileDiffOutput> priorVsCurrent = null;
+ Map<String, ModifiedFile> baseVsCurrent = null;
+ Map<String, ModifiedFile> baseVsPrior = null;
+ Map<String, ModifiedFile> priorVsCurrent = null;
LabelTypes labelTypes = project.getLabelTypes();
- for (PatchSetApproval psa : priorApprovals) {
+ for (PatchSetApproval psa : priorApprovalsIncludingCopied) {
if (resultByUser.contains(psa.label(), psa.accountId())) {
continue;
}
@@ -418,10 +397,11 @@
if (baseVsCurrent == null
&& type.isPresent()
&& type.get().isCopyAllScoresIfListOfFilesDidNotChange()) {
- baseVsCurrent = listModifiedFiles(project, patchSet);
- baseVsPrior = listModifiedFiles(project, priorPatchSet.getValue());
+ baseVsCurrent = listModifiedFiles(project, patchSet, rw, repoConfig);
+ baseVsPrior = listModifiedFiles(project, priorPatchSet.getValue(), rw, repoConfig);
priorVsCurrent =
- listModifiedFiles(project, priorPatchSet.getValue().commitId(), patchSet.commitId());
+ listModifiedFiles(
+ project, priorPatchSet.getValue().commitId(), patchSet.commitId(), rw, repoConfig);
}
if (!type.isPresent()) {
logger.atFine().log(
@@ -444,7 +424,8 @@
baseVsCurrent,
baseVsPrior,
priorVsCurrent)
- && !canCopyBasedOnCopyCondition(notes, psa, patchSet, type.get(), changeKind)) {
+ && !canCopyBasedOnCopyCondition(
+ notes, psa, patchSet, type.get(), changeKind, rw, repoConfig)) {
continue;
}
resultByUser.put(psa.label(), psa.accountId(), psa.copyWithPatchSet(patchSet.id()));
@@ -456,14 +437,20 @@
* Gets the modified files between the two latest patch-sets. Can be used to compute difference in
* files between those two patch-sets .
*/
- private Map<String, FileDiffOutput> listModifiedFiles(ProjectState project, PatchSet ps) {
+ private Map<String, ModifiedFile> listModifiedFiles(
+ ProjectState project, PatchSet ps, RevWalk revWalk, Config repoConfig) {
try {
Integer parentNum =
listOfFilesUnchangedPredicate.isInitialCommit(project.getNameKey(), ps.commitId())
? 0
: 1;
- return diffOperations.listModifiedFilesAgainstParent(
- project.getNameKey(), ps.commitId(), parentNum);
+ return diffOperations.loadModifiedFilesAgainstParent(
+ project.getNameKey(),
+ ps.commitId(),
+ parentNum,
+ DiffOptions.DEFAULTS,
+ revWalk,
+ repoConfig);
} catch (DiffNotAvailableException ex) {
throw new StorageException(
"failed to compute difference in files, so won't copy"
@@ -477,10 +464,20 @@
* Gets the modified files between two commits corresponding to different patchsets of the same
* change.
*/
- private Map<String, FileDiffOutput> listModifiedFiles(
- ProjectState project, ObjectId sourceCommit, ObjectId targetCommit) {
+ private Map<String, ModifiedFile> listModifiedFiles(
+ ProjectState project,
+ ObjectId sourceCommit,
+ ObjectId targetCommit,
+ RevWalk revWalk,
+ Config repoConfig) {
try {
- return diffOperations.listModifiedFiles(project.getNameKey(), sourceCommit, targetCommit);
+ return diffOperations.loadModifiedFiles(
+ project.getNameKey(),
+ sourceCommit,
+ targetCommit,
+ DiffOptions.DEFAULTS,
+ revWalk,
+ repoConfig);
} catch (DiffNotAvailableException ex) {
throw new StorageException(
"failed to compute difference in files, so won't copy"
diff --git a/java/com/google/gerrit/server/approval/ApprovalsUtil.java b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
index c2e35d2..8d227f7 100644
--- a/java/com/google/gerrit/server/approval/ApprovalsUtil.java
+++ b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
@@ -26,7 +26,6 @@
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.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelId;
@@ -42,6 +41,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
+import com.google.gerrit.server.change.LabelNormalizer;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
@@ -53,10 +53,10 @@
import com.google.gerrit.server.util.LabelVote;
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -80,7 +80,7 @@
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public static PatchSetApproval.Builder newApproval(
- PatchSet.Id psId, CurrentUser user, LabelId labelId, int value, Date when) {
+ PatchSet.Id psId, CurrentUser user, LabelId labelId, int value, Instant when) {
PatchSetApproval.Builder b =
PatchSetApproval.builder()
.key(PatchSetApproval.key(psId, user.getAccountId(), labelId))
@@ -98,7 +98,7 @@
private final ApprovalInference approvalInference;
private final PermissionBackend permissionBackend;
private final ProjectCache projectCache;
- private final ApprovalCache approvalCache;
+ private final LabelNormalizer labelNormalizer;
@VisibleForTesting
@Inject
@@ -106,11 +106,11 @@
ApprovalInference approvalInference,
PermissionBackend permissionBackend,
ProjectCache projectCache,
- ApprovalCache approvalCache) {
+ LabelNormalizer labelNormalizer) {
this.approvalInference = approvalInference;
this.permissionBackend = permissionBackend;
this.projectCache = projectCache;
- this.approvalCache = approvalCache;
+ this.labelNormalizer = labelNormalizer;
}
/**
@@ -297,7 +297,7 @@
}
checkApprovals(approvals, permissionBackend.user(user).change(update.getNotes()));
List<PatchSetApproval> cells = new ArrayList<>(approvals.size());
- Date ts = update.getWhen();
+ Instant ts = update.getWhen();
for (Map.Entry<String, Short> vote : approvals.entrySet()) {
Optional<LabelType> lt = labelTypes.byLabel(vote.getKey());
if (!lt.isPresent()) {
@@ -339,30 +339,42 @@
}
}
- public ListMultimap<PatchSet.Id, PatchSetApproval> byChange(ChangeNotes notes) {
+ public ListMultimap<PatchSet.Id, PatchSetApproval> byChangeExcludingCopiedApprovals(
+ ChangeNotes notes) {
return notes.load().getApprovals();
}
- public Iterable<PatchSetApproval> byPatchSet(
- ChangeNotes notes, PatchSet.Id psId, @Nullable RevWalk rw, @Nullable Config repoConfig) {
- return approvalInference.forPatchSet(notes, psId, rw, repoConfig);
- }
-
- public Iterable<PatchSetApproval> byPatchSet(ChangeNotes notes, PatchSet patchSet) {
- return approvalInference.forPatchSet(notes, patchSet, /* rw= */ null, /* repoConfig= */ null);
- }
-
- public Iterable<PatchSetApproval> byPatchSet(ChangeNotes notes, PatchSet.Id psId) {
- return approvalCache.get(notes, psId);
- }
-
- public Iterable<PatchSetApproval> byPatchSetUser(
+ /**
+ * This method should only be used when we want to dynamically compute the approvals. Generally,
+ * the copied approvals are available in {@link ChangeNotes}. However, if the patch-set is just
+ * being created, we need to dynamically compute the approvals so that we can persist them in
+ * storage. The {@link RevWalk} and {@link Config} objects that are being used to create the new
+ * patch-set are required for this method. Here we also add those votes to the provided {@link
+ * ChangeUpdate} object.
+ */
+ public void persistCopiedApprovals(
ChangeNotes notes,
- PatchSet.Id psId,
- Account.Id accountId,
- @Nullable RevWalk rw,
- @Nullable Config repoConfig) {
- return filterApprovals(byPatchSet(notes, psId, rw, repoConfig), accountId);
+ PatchSet patchSet,
+ RevWalk revWalk,
+ Config repoConfig,
+ ChangeUpdate changeUpdate) {
+ approvalInference
+ .forPatchSet(notes, patchSet, revWalk, repoConfig)
+ .forEach(a -> changeUpdate.putCopiedApproval(a));
+ }
+
+ /**
+ * Gets {@link PatchSetApproval}s for a specified patch-set. The result includes copied votes but
+ * does not include deleted labels.
+ *
+ * @param notes changenotes of the change.
+ * @param psId patch-set id for the change and patch-set we want to get approvals.
+ * @return all approvals for the specified patch-set, including copied votes, not including
+ * deleted labels.
+ */
+ public Iterable<PatchSetApproval> byPatchSet(ChangeNotes notes, PatchSet.Id psId) {
+ List<PatchSetApproval> approvalsNotNormalized = notes.load().getApprovalsWithCopied().get(psId);
+ return labelNormalizer.normalize(notes, approvalsNotNormalized).getNormalized();
}
public Iterable<PatchSetApproval> byPatchSetUser(
@@ -375,8 +387,8 @@
return null;
}
try {
- // Submit approval is never copied, so bypass expensive byPatchSet call.
- return getSubmitter(c, byChange(notes).get(c));
+ // Submit approval is never copied.
+ return getSubmitter(c, byChangeExcludingCopiedApprovals(notes).get(c));
} catch (StorageException e) {
return null;
}
diff --git a/java/com/google/gerrit/server/approval/PatchSetApprovalUuidGenerator.java b/java/com/google/gerrit/server/approval/PatchSetApprovalUuidGenerator.java
new file mode 100644
index 0000000..128bee4
--- /dev/null
+++ b/java/com/google/gerrit/server/approval/PatchSetApprovalUuidGenerator.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.approval;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.PatchSetApproval.UUID;
+import com.google.inject.ImplementedBy;
+import java.time.Instant;
+
+/**
+ * Generator for {@link com.google.gerrit.entities.PatchSetApproval.UUID}.
+ *
+ * <p>Since {@link com.google.gerrit.entities.PatchSetApproval.UUID} must be unique for each granted
+ * {@link PatchSetApproval}, implementations must generate globally unique UUID for each {@link
+ * #get} invocation.
+ */
+@ImplementedBy(PatchSetApprovalUuidGeneratorImpl.class)
+public interface PatchSetApprovalUuidGenerator {
+
+ /**
+ * Generates {@link com.google.gerrit.entities.PatchSetApproval.UUID} based on the properties of
+ * {@link PatchSetApproval} that is being granted.
+ */
+ UUID get(
+ PatchSet.Id patchSetId, Account.Id accountId, String label, short value, Instant granted);
+}
diff --git a/java/com/google/gerrit/server/approval/PatchSetApprovalUuidGeneratorImpl.java b/java/com/google/gerrit/server/approval/PatchSetApprovalUuidGeneratorImpl.java
new file mode 100644
index 0000000..afa0384
--- /dev/null
+++ b/java/com/google/gerrit/server/approval/PatchSetApprovalUuidGeneratorImpl.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.approval;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.PatchSetApproval.UUID;
+import com.google.inject.Singleton;
+import java.security.MessageDigest;
+import java.time.Instant;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * Default implementation of {@link
+ * com.google.gerrit.server.approval.PatchSetApprovalUuidGenerator}.
+ */
+@Singleton
+public class PatchSetApprovalUuidGeneratorImpl implements PatchSetApprovalUuidGenerator {
+ @Override
+ public UUID get(
+ PatchSet.Id patchSetId, Account.Id accountId, String label, short value, Instant granted) {
+ MessageDigest md = Constants.newMessageDigest();
+ md.update(
+ Constants.encode("patchSetId " + patchSetId.getCommaSeparatedChangeAndPatchSetId() + "\n"));
+ md.update(Constants.encode("accountId " + accountId + "\n"));
+ md.update(Constants.encode("label " + label + "\n"));
+ md.update(Constants.encode("value " + value + "\n"));
+ md.update(Constants.encode("granted " + granted.toEpochMilli() + "\n"));
+ md.update(Constants.encode(String.valueOf(Math.random())));
+ return PatchSetApproval.uuid(ObjectId.fromRaw(md.digest()).name());
+ }
+}
diff --git a/java/com/google/gerrit/server/approval/testing/TestPatchSetApprovalUuidGenerator.java b/java/com/google/gerrit/server/approval/testing/TestPatchSetApprovalUuidGenerator.java
new file mode 100644
index 0000000..f6527d2
--- /dev/null
+++ b/java/com/google/gerrit/server/approval/testing/TestPatchSetApprovalUuidGenerator.java
@@ -0,0 +1,35 @@
+package com.google.gerrit.server.approval.testing;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.entities.PatchSetApproval.UUID;
+import com.google.gerrit.server.approval.PatchSetApprovalUuidGenerator;
+import java.time.Instant;
+import javax.inject.Singleton;
+
+/**
+ * Implementation of {@link PatchSetApprovalUuidGenerator} that returns predictable {@link UUID}.
+ */
+@Singleton
+public class TestPatchSetApprovalUuidGenerator implements PatchSetApprovalUuidGenerator {
+
+ private int invocationCount = 0;
+
+ @Override
+ public UUID get(
+ PatchSet.Id patchSetId, Account.Id accountId, String label, short value, Instant granted) {
+ invocationCount++;
+ return PatchSetApproval.uuid(
+ String.format(
+ "%s_%s_%s_%s_%s_%s",
+ patchSetId.changeId().get(),
+ patchSetId.get(),
+ accountId.get(),
+ label,
+ value,
+ invocationCount)
+ .replace("-", "_")
+ .toLowerCase());
+ }
+}
diff --git a/java/com/google/gerrit/server/args4j/AccountIdHandler.java b/java/com/google/gerrit/server/args4j/AccountIdHandler.java
index 5df4d28..94dc4c3 100644
--- a/java/com/google/gerrit/server/args4j/AccountIdHandler.java
+++ b/java/com/google/gerrit/server/args4j/AccountIdHandler.java
@@ -90,9 +90,9 @@
}
}
} catch (StorageException e) {
- String msg = "database is down";
- logger.atSevere().withCause(e).log(msg);
- throw new CmdLineException(owner, localizable(msg));
+ CmdLineException newException = new CmdLineException(owner, localizable("database is down"));
+ newException.initCause(e);
+ throw newException;
} catch (IOException e) {
throw new CmdLineException(owner, "Failed to load account", e);
} catch (ConfigInvalidException e) {
diff --git a/java/com/google/gerrit/server/args4j/TimestampHandler.java b/java/com/google/gerrit/server/args4j/InstantHandler.java
similarity index 73%
rename from java/com/google/gerrit/server/args4j/TimestampHandler.java
rename to java/com/google/gerrit/server/args4j/InstantHandler.java
index eddfbcd..bfca0f6 100644
--- a/java/com/google/gerrit/server/args4j/TimestampHandler.java
+++ b/java/com/google/gerrit/server/args4j/InstantHandler.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2021 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -16,11 +16,10 @@
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import java.sql.Timestamp;
-import java.text.DateFormat;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.TimeZone;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.OptionDef;
@@ -28,14 +27,14 @@
import org.kohsuke.args4j.spi.Parameters;
import org.kohsuke.args4j.spi.Setter;
-public class TimestampHandler extends OptionHandler<Timestamp> {
+public class InstantHandler extends OptionHandler<Instant> {
public static final String TIMESTAMP_FORMAT = "yyyyMMdd_HHmm";
@Inject
- public TimestampHandler(
+ public InstantHandler(
@Assisted CmdLineParser parser,
@Assisted OptionDef option,
- @Assisted Setter<Timestamp> setter) {
+ @Assisted Setter<Instant> setter) {
super(parser, option, setter);
}
@@ -43,11 +42,12 @@
public int parseArguments(Parameters params) throws CmdLineException {
String timestamp = params.getParameter(0);
try {
- DateFormat fmt = new SimpleDateFormat(TIMESTAMP_FORMAT);
- fmt.setTimeZone(TimeZone.getTimeZone("UTC"));
- setter.addValue(new Timestamp(fmt.parse(timestamp).getTime()));
+ setter.addValue(
+ DateTimeFormatter.ofPattern(TIMESTAMP_FORMAT)
+ .withZone(ZoneId.of("UTC"))
+ .parse(timestamp, Instant::from));
return 1;
- } catch (ParseException e) {
+ } catch (DateTimeParseException e) {
throw new CmdLineException(
owner,
String.format("Invalid timestamp: %s; expected format: %s", timestamp, TIMESTAMP_FORMAT),
diff --git a/java/com/google/gerrit/server/audit/AuditService.java b/java/com/google/gerrit/server/audit/AuditService.java
index 2a5d868..695c32a 100644
--- a/java/com/google/gerrit/server/audit/AuditService.java
+++ b/java/com/google/gerrit/server/audit/AuditService.java
@@ -25,7 +25,7 @@
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.inject.Inject;
import com.google.inject.Singleton;
-import java.sql.Timestamp;
+import java.time.Instant;
@Singleton
public class AuditService implements GroupAuditService {
@@ -50,7 +50,7 @@
Account.Id actor,
AccountGroup.UUID updatedGroup,
ImmutableSet<Account.Id> addedMembers,
- Timestamp addedOn) {
+ Instant addedOn) {
GroupMemberAuditEvent event =
GroupMemberAuditEvent.create(actor, updatedGroup, addedMembers, addedOn);
groupAuditListeners.runEach(l -> l.onAddMembers(event));
@@ -61,7 +61,7 @@
Account.Id actor,
AccountGroup.UUID updatedGroup,
ImmutableSet<Account.Id> deletedMembers,
- Timestamp deletedOn) {
+ Instant deletedOn) {
GroupMemberAuditEvent event =
GroupMemberAuditEvent.create(actor, updatedGroup, deletedMembers, deletedOn);
groupAuditListeners.runEach(l -> l.onDeleteMembers(event));
@@ -72,7 +72,7 @@
Account.Id actor,
AccountGroup.UUID updatedGroup,
ImmutableSet<AccountGroup.UUID> addedSubgroups,
- Timestamp addedOn) {
+ Instant addedOn) {
GroupSubgroupAuditEvent event =
GroupSubgroupAuditEvent.create(actor, updatedGroup, addedSubgroups, addedOn);
groupAuditListeners.runEach(l -> l.onAddSubgroups(event));
@@ -83,7 +83,7 @@
Account.Id actor,
AccountGroup.UUID updatedGroup,
ImmutableSet<AccountGroup.UUID> deletedSubgroups,
- Timestamp deletedOn) {
+ Instant deletedOn) {
GroupSubgroupAuditEvent event =
GroupSubgroupAuditEvent.create(actor, updatedGroup, deletedSubgroups, deletedOn);
groupAuditListeners.runEach(l -> l.onDeleteSubgroups(event));
diff --git a/java/com/google/gerrit/server/audit/BUILD b/java/com/google/gerrit/server/audit/BUILD
index 3faa259..9ed5879 100644
--- a/java/com/google/gerrit/server/audit/BUILD
+++ b/java/com/google/gerrit/server/audit/BUILD
@@ -66,7 +66,6 @@
"//lib/guice:guice-assistedinject",
"//lib/guice:guice-servlet",
"//lib/jsoup",
- "//lib/log:log4j",
"//lib/lucene:lucene-analyzers-common",
"//lib/lucene:lucene-queryparser",
"//lib/mime4j:core",
diff --git a/java/com/google/gerrit/server/audit/group/GroupAuditEvent.java b/java/com/google/gerrit/server/audit/group/GroupAuditEvent.java
index 252a1e2..c37c583 100644
--- a/java/com/google/gerrit/server/audit/group/GroupAuditEvent.java
+++ b/java/com/google/gerrit/server/audit/group/GroupAuditEvent.java
@@ -16,7 +16,7 @@
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
-import java.sql.Timestamp;
+import java.time.Instant;
/** An audit event for groups. */
public interface GroupAuditEvent {
@@ -35,9 +35,9 @@
AccountGroup.UUID getUpdatedGroup();
/**
- * Gets the {@link Timestamp} of the action.
+ * Gets the {@link Instant} of the action.
*
- * @return the {@link Timestamp} of the action.
+ * @return the {@link Instant} of the action.
*/
- Timestamp getTimestamp();
+ Instant getTimestamp();
}
diff --git a/java/com/google/gerrit/server/audit/group/GroupMemberAuditEvent.java b/java/com/google/gerrit/server/audit/group/GroupMemberAuditEvent.java
index eccfbf4..95a2dce 100644
--- a/java/com/google/gerrit/server/audit/group/GroupMemberAuditEvent.java
+++ b/java/com/google/gerrit/server/audit/group/GroupMemberAuditEvent.java
@@ -18,7 +18,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
-import java.sql.Timestamp;
+import java.time.Instant;
@AutoValue
public abstract class GroupMemberAuditEvent implements GroupAuditEvent {
@@ -26,7 +26,7 @@
Account.Id actor,
AccountGroup.UUID updatedGroup,
ImmutableSet<Account.Id> modifiedMembers,
- Timestamp timestamp) {
+ Instant timestamp) {
return new AutoValue_GroupMemberAuditEvent(actor, updatedGroup, modifiedMembers, timestamp);
}
@@ -40,5 +40,5 @@
public abstract ImmutableSet<Account.Id> getModifiedMembers();
@Override
- public abstract Timestamp getTimestamp();
+ public abstract Instant getTimestamp();
}
diff --git a/java/com/google/gerrit/server/audit/group/GroupSubgroupAuditEvent.java b/java/com/google/gerrit/server/audit/group/GroupSubgroupAuditEvent.java
index 0fe3962..ace8312 100644
--- a/java/com/google/gerrit/server/audit/group/GroupSubgroupAuditEvent.java
+++ b/java/com/google/gerrit/server/audit/group/GroupSubgroupAuditEvent.java
@@ -18,7 +18,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
-import java.sql.Timestamp;
+import java.time.Instant;
@AutoValue
public abstract class GroupSubgroupAuditEvent implements GroupAuditEvent {
@@ -26,7 +26,7 @@
Account.Id actor,
AccountGroup.UUID updatedGroup,
ImmutableSet<AccountGroup.UUID> modifiedSubgroups,
- Timestamp timestamp) {
+ Instant timestamp) {
return new AutoValue_GroupSubgroupAuditEvent(actor, updatedGroup, modifiedSubgroups, timestamp);
}
@@ -40,5 +40,5 @@
public abstract ImmutableSet<AccountGroup.UUID> getModifiedSubgroups();
@Override
- public abstract Timestamp getTimestamp();
+ public abstract Instant getTimestamp();
}
diff --git a/java/com/google/gerrit/server/cache/CacheBackend.java b/java/com/google/gerrit/server/cache/CacheBackend.java
deleted file mode 100644
index ec9876f..0000000
--- a/java/com/google/gerrit/server/cache/CacheBackend.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2019 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.cache;
-
-/** Caffeine is used as default cache backend, but can be overridden with Guava backend. */
-public enum CacheBackend {
- CAFFEINE,
- GUAVA;
-
- public boolean isLegacyBackend() {
- return this == GUAVA;
- }
-}
diff --git a/java/com/google/gerrit/server/cache/CacheModule.java b/java/com/google/gerrit/server/cache/CacheModule.java
index 0fdc6f5..d4e4509 100644
--- a/java/com/google/gerrit/server/cache/CacheModule.java
+++ b/java/com/google/gerrit/server/cache/CacheModule.java
@@ -35,7 +35,7 @@
public static final String MEMORY_MODULE = "cache-memory";
public static final String PERSISTENT_MODULE = "cache-persistent";
- private static final TypeLiteral<Cache<?, ?>> ANY_CACHE = new TypeLiteral<Cache<?, ?>>() {};
+ private static final TypeLiteral<Cache<?, ?>> ANY_CACHE = new TypeLiteral<>() {};
/**
* Declare a named in-memory cache.
@@ -68,8 +68,7 @@
*/
protected <K, V> CacheBinding<K, V> cache(
String name, TypeLiteral<K> keyType, TypeLiteral<V> valType) {
- CacheProvider<K, V> m =
- new CacheProvider<>(this, name, keyType, valType, CacheBackend.CAFFEINE);
+ CacheProvider<K, V> m = new CacheProvider<>(this, name, keyType, valType);
bindCache(m, name, keyType, valType);
return m;
}
@@ -124,20 +123,7 @@
*/
protected <K, V> PersistentCacheBinding<K, V> persist(
String name, Class<K> keyType, Class<V> valType) {
- return persist(name, TypeLiteral.get(keyType), TypeLiteral.get(valType), CacheBackend.CAFFEINE);
- }
-
- /**
- * Declare a named in-memory/on-disk cache.
- *
- * @param <K> type of key used to lookup entries.
- * @param <V> type of value stored by the cache.
- * @param backend cache backend.
- * @return binding to describe the cache.
- */
- protected <K, V> PersistentCacheBinding<K, V> persist(
- String name, Class<K> keyType, Class<V> valType, CacheBackend backend) {
- return persist(name, TypeLiteral.get(keyType), TypeLiteral.get(valType), backend);
+ return persist(name, TypeLiteral.get(keyType), TypeLiteral.get(valType));
}
/**
@@ -149,7 +135,7 @@
*/
protected <K, V> PersistentCacheBinding<K, V> persist(
String name, Class<K> keyType, TypeLiteral<V> valType) {
- return persist(name, TypeLiteral.get(keyType), valType, CacheBackend.CAFFEINE);
+ return persist(name, TypeLiteral.get(keyType), valType);
}
/**
@@ -160,9 +146,8 @@
* @return binding to describe the cache.
*/
protected <K, V> PersistentCacheBinding<K, V> persist(
- String name, TypeLiteral<K> keyType, TypeLiteral<V> valType, CacheBackend backend) {
- PersistentCacheProvider<K, V> m =
- new PersistentCacheProvider<>(this, name, keyType, valType, backend);
+ String name, TypeLiteral<K> keyType, TypeLiteral<V> valType) {
+ PersistentCacheProvider<K, V> m = new PersistentCacheProvider<>(this, name, keyType, valType);
bindCache(m, name, keyType, valType);
Type cacheDefType =
diff --git a/java/com/google/gerrit/server/cache/CacheProvider.java b/java/com/google/gerrit/server/cache/CacheProvider.java
index 2dd9e1f..94504b6 100644
--- a/java/com/google/gerrit/server/cache/CacheProvider.java
+++ b/java/com/google/gerrit/server/cache/CacheProvider.java
@@ -30,7 +30,6 @@
class CacheProvider<K, V> implements Provider<Cache<K, V>>, CacheBinding<K, V>, CacheDef<K, V> {
private final CacheModule module;
- private final CacheBackend backend;
final String name;
private final TypeLiteral<K> keyType;
private final TypeLiteral<V> valType;
@@ -46,17 +45,11 @@
private MemoryCacheFactory memoryCacheFactory;
private boolean frozen;
- CacheProvider(
- CacheModule module,
- String name,
- TypeLiteral<K> keyType,
- TypeLiteral<V> valType,
- CacheBackend backend) {
+ CacheProvider(CacheModule module, String name, TypeLiteral<K> keyType, TypeLiteral<V> valType) {
this.module = module;
this.name = name;
this.keyType = keyType;
this.valType = valType;
- this.backend = backend;
}
@Inject(optional = true)
@@ -179,9 +172,7 @@
public Cache<K, V> get() {
freeze();
CacheLoader<K, V> ldr = loader();
- return ldr != null
- ? memoryCacheFactory.build(this, ldr, backend)
- : memoryCacheFactory.build(this, backend);
+ return ldr != null ? memoryCacheFactory.build(this, ldr) : memoryCacheFactory.build(this);
}
protected void checkNotFrozen() {
diff --git a/java/com/google/gerrit/server/cache/MemoryCacheFactory.java b/java/com/google/gerrit/server/cache/MemoryCacheFactory.java
index 558380d..fc55753 100644
--- a/java/com/google/gerrit/server/cache/MemoryCacheFactory.java
+++ b/java/com/google/gerrit/server/cache/MemoryCacheFactory.java
@@ -19,8 +19,7 @@
import com.google.common.cache.LoadingCache;
public interface MemoryCacheFactory {
- <K, V> Cache<K, V> build(CacheDef<K, V> def, CacheBackend backend);
+ <K, V> Cache<K, V> build(CacheDef<K, V> def);
- <K, V> LoadingCache<K, V> build(
- CacheDef<K, V> def, CacheLoader<K, V> loader, CacheBackend backend);
+ <K, V> LoadingCache<K, V> build(CacheDef<K, V> def, CacheLoader<K, V> loader);
}
diff --git a/java/com/google/gerrit/server/cache/PersistentCacheBaseFactory.java b/java/com/google/gerrit/server/cache/PersistentCacheBaseFactory.java
index 9553acc..ec527ba 100644
--- a/java/com/google/gerrit/server/cache/PersistentCacheBaseFactory.java
+++ b/java/com/google/gerrit/server/cache/PersistentCacheBaseFactory.java
@@ -45,33 +45,31 @@
this.config = config;
}
- protected abstract <K, V> Cache<K, V> buildImpl(
- PersistentCacheDef<K, V> in, long diskLimit, CacheBackend backend);
+ protected abstract <K, V> Cache<K, V> buildImpl(PersistentCacheDef<K, V> in, long diskLimit);
protected abstract <K, V> LoadingCache<K, V> buildImpl(
- PersistentCacheDef<K, V> in, CacheLoader<K, V> loader, long diskLimit, CacheBackend backend);
+ PersistentCacheDef<K, V> in, CacheLoader<K, V> loader, long diskLimit);
@Override
- public <K, V> Cache<K, V> build(PersistentCacheDef<K, V> in, CacheBackend backend) {
+ public <K, V> Cache<K, V> build(PersistentCacheDef<K, V> in) {
long limit = getDiskLimit(in);
if (isInMemoryCache(limit)) {
- return memCacheFactory.build(in, backend);
+ return memCacheFactory.build(in);
}
- return buildImpl(in, limit, backend);
+ return buildImpl(in, limit);
}
@Override
- public <K, V> LoadingCache<K, V> build(
- PersistentCacheDef<K, V> in, CacheLoader<K, V> loader, CacheBackend backend) {
+ public <K, V> LoadingCache<K, V> build(PersistentCacheDef<K, V> in, CacheLoader<K, V> loader) {
long limit = getDiskLimit(in);
if (isInMemoryCache(limit)) {
- return memCacheFactory.build(in, loader, backend);
+ return memCacheFactory.build(in, loader);
}
- return buildImpl(in, loader, limit, backend);
+ return buildImpl(in, loader, limit);
}
private <K, V> long getDiskLimit(PersistentCacheDef<K, V> in) {
diff --git a/java/com/google/gerrit/server/cache/PersistentCacheFactory.java b/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
index 93f91ef..27fa9ca 100644
--- a/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
+++ b/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
@@ -19,10 +19,9 @@
import com.google.common.cache.LoadingCache;
public interface PersistentCacheFactory {
- <K, V> Cache<K, V> build(PersistentCacheDef<K, V> def, CacheBackend backend);
+ <K, V> Cache<K, V> build(PersistentCacheDef<K, V> def);
- <K, V> LoadingCache<K, V> build(
- PersistentCacheDef<K, V> def, CacheLoader<K, V> loader, CacheBackend backend);
+ <K, V> LoadingCache<K, V> build(PersistentCacheDef<K, V> def, CacheLoader<K, V> loader);
void onStop(String plugin);
}
diff --git a/java/com/google/gerrit/server/cache/PersistentCacheProvider.java b/java/com/google/gerrit/server/cache/PersistentCacheProvider.java
index 4fc107f..59d66e3 100644
--- a/java/com/google/gerrit/server/cache/PersistentCacheProvider.java
+++ b/java/com/google/gerrit/server/cache/PersistentCacheProvider.java
@@ -30,7 +30,6 @@
class PersistentCacheProvider<K, V> extends CacheProvider<K, V>
implements Provider<Cache<K, V>>, PersistentCacheBinding<K, V>, PersistentCacheDef<K, V> {
- private final CacheBackend backend;
private int version;
private long diskLimit;
private CacheSerializer<K> keySerializer;
@@ -40,19 +39,9 @@
PersistentCacheProvider(
CacheModule module, String name, TypeLiteral<K> keyType, TypeLiteral<V> valType) {
- this(module, name, keyType, valType, CacheBackend.CAFFEINE);
- }
-
- PersistentCacheProvider(
- CacheModule module,
- String name,
- TypeLiteral<K> keyType,
- TypeLiteral<V> valType,
- CacheBackend backend) {
- super(module, name, keyType, valType, backend);
+ super(module, name, keyType, valType);
version = -1;
diskLimit = 128 << 20;
- this.backend = backend;
}
@Inject(optional = true)
@@ -141,8 +130,8 @@
freeze();
CacheLoader<K, V> ldr = loader();
return ldr != null
- ? persistentCacheFactory.build(this, ldr, backend)
- : persistentCacheFactory.build(this, backend);
+ ? persistentCacheFactory.build(this, ldr)
+ : persistentCacheFactory.build(this);
}
private static <T> void checkSerializer(
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index 16d62b3..445d8a0 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -21,7 +21,6 @@
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.server.cache.CacheBackend;
import com.google.gerrit.server.cache.MemoryCacheFactory;
import com.google.gerrit.server.cache.PersistentCacheBaseFactory;
import com.google.gerrit.server.cache.PersistentCacheDef;
@@ -34,7 +33,7 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
-import java.util.LinkedList;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
@@ -68,7 +67,7 @@
super(memCacheFactory, cfg, site);
h2CacheSize = cfg.getLong("cache", null, "h2CacheSize", -1);
h2AutoServer = cfg.getBoolean("cache", null, "h2AutoServer", false);
- caches = new LinkedList<>();
+ caches = new ArrayList<>();
this.cacheMap = cacheMap;
if (diskEnabled) {
@@ -132,34 +131,28 @@
@SuppressWarnings({"unchecked"})
@Override
- public <K, V> Cache<K, V> buildImpl(
- PersistentCacheDef<K, V> in, long limit, CacheBackend backend) {
+ public <K, V> Cache<K, V> buildImpl(PersistentCacheDef<K, V> in, long limit) {
H2CacheDefProxy<K, V> def = new H2CacheDefProxy<>(in);
SqlStore<K, V> store = newSqlStore(def, limit);
H2CacheImpl<K, V> cache =
new H2CacheImpl<>(
- executor,
- store,
- def.keyType(),
- (Cache<K, ValueHolder<V>>) memCacheFactory.build(def, backend));
+ executor, store, def.keyType(), (Cache<K, ValueHolder<V>>) memCacheFactory.build(def));
synchronized (caches) {
caches.add(cache);
}
return cache;
}
- @SuppressWarnings("unchecked")
+ @SuppressWarnings({"unchecked"})
@Override
public <K, V> LoadingCache<K, V> buildImpl(
- PersistentCacheDef<K, V> in, CacheLoader<K, V> loader, long limit, CacheBackend backend) {
+ PersistentCacheDef<K, V> in, CacheLoader<K, V> loader, long limit) {
H2CacheDefProxy<K, V> def = new H2CacheDefProxy<>(in);
SqlStore<K, V> store = newSqlStore(def, limit);
Cache<K, ValueHolder<V>> mem =
(Cache<K, ValueHolder<V>>)
memCacheFactory.build(
- def,
- (CacheLoader<K, V>) new H2CacheImpl.Loader<>(executor, store, loader),
- backend);
+ def, (CacheLoader<K, V>) new H2CacheImpl.Loader<>(executor, store, loader));
H2CacheImpl<K, V> cache = new H2CacheImpl<>(executor, store, def.keyType(), mem);
synchronized (caches) {
caches.add(cache);
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 13b8b12..0403408 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -548,7 +548,7 @@
c.touch = c.conn.prepareStatement("UPDATE data SET accessed=? WHERE k=? AND version=?");
}
try {
- c.touch.setTimestamp(1, TimeUtil.nowTs());
+ c.touch.setTimestamp(1, new Timestamp(TimeUtil.nowMs()));
keyType.set(c.touch, 2, key);
c.touch.setInt(3, version);
c.touch.executeUpdate();
@@ -581,7 +581,7 @@
c.put.setBytes(2, valueSerializer.serialize(holder.value));
c.put.setInt(3, version);
c.put.setTimestamp(4, Timestamp.from(holder.created));
- c.put.setTimestamp(5, TimeUtil.nowTs());
+ c.put.setTimestamp(5, new Timestamp(TimeUtil.nowMs()));
c.put.executeUpdate();
holder.clean = true;
} finally {
diff --git a/java/com/google/gerrit/server/cache/h2/ObjectKeyTypeImpl.java b/java/com/google/gerrit/server/cache/h2/ObjectKeyTypeImpl.java
index 591883e..1812043 100644
--- a/java/com/google/gerrit/server/cache/h2/ObjectKeyTypeImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/ObjectKeyTypeImpl.java
@@ -47,7 +47,7 @@
@Override
public Funnel<K> funnel() {
- return new Funnel<K>() {
+ return new Funnel<>() {
private static final long serialVersionUID = 1L;
@Override
diff --git a/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java b/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
index 23caca7..f672d11 100644
--- a/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
+++ b/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
@@ -23,12 +23,10 @@
import com.github.benmanes.caffeine.guava.CaffeinatedGuava;
import com.google.common.base.Strings;
import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalNotification;
import com.google.gerrit.common.Nullable;
-import com.google.gerrit.server.cache.CacheBackend;
import com.google.gerrit.server.cache.CacheDef;
import com.google.gerrit.server.cache.ForwardingRemovalListener;
import com.google.gerrit.server.cache.MemoryCacheFactory;
@@ -51,76 +49,13 @@
}
@Override
- public <K, V> Cache<K, V> build(CacheDef<K, V> def, CacheBackend backend) {
- return backend.isLegacyBackend()
- ? createLegacy(def).build()
- : CaffeinatedGuava.build(create(def));
+ public <K, V> Cache<K, V> build(CacheDef<K, V> def) {
+ return CaffeinatedGuava.build(create(def));
}
@Override
- public <K, V> LoadingCache<K, V> build(
- CacheDef<K, V> def, CacheLoader<K, V> loader, CacheBackend backend) {
- return backend.isLegacyBackend()
- ? createLegacy(def).build(loader)
- : CaffeinatedGuava.build(create(def), loader);
- }
-
- @SuppressWarnings("unchecked")
- private <K, V> CacheBuilder<K, V> createLegacy(CacheDef<K, V> def) {
- CacheBuilder<K, V> builder = newLegacyCacheBuilder();
- builder.recordStats();
- builder.maximumWeight(
- cfg.getLong("cache", def.configKey(), "memoryLimit", def.maximumWeight()));
-
- builder = builder.removalListener(forwardingRemovalListenerFactory.create(def.name()));
-
- com.google.common.cache.Weigher<K, V> weigher = def.weigher();
- if (weigher == null) {
- weigher = unitWeight();
- }
- builder.weigher(weigher);
-
- Duration expireAfterWrite = def.expireAfterWrite();
- if (has(def.configKey(), "maxAge")) {
- builder.expireAfterWrite(
- ConfigUtil.getTimeUnit(
- cfg, "cache", def.configKey(), "maxAge", toSeconds(expireAfterWrite), SECONDS),
- SECONDS);
- } else if (expireAfterWrite != null) {
- builder.expireAfterWrite(expireAfterWrite.toNanos(), NANOSECONDS);
- }
-
- Duration expireAfterAccess = def.expireFromMemoryAfterAccess();
- if (has(def.configKey(), "expireFromMemoryAfterAccess")) {
- builder.expireAfterAccess(
- ConfigUtil.getTimeUnit(
- cfg,
- "cache",
- def.configKey(),
- "expireFromMemoryAfterAccess",
- toSeconds(expireAfterAccess),
- SECONDS),
- SECONDS);
- } else if (expireAfterAccess != null) {
- builder.expireAfterAccess(expireAfterAccess.toNanos(), NANOSECONDS);
- }
-
- Duration refreshAfterWrite = def.refreshAfterWrite();
- if (has(def.configKey(), "refreshAfterWrite")) {
- builder.refreshAfterWrite(
- ConfigUtil.getTimeUnit(
- cfg,
- "cache",
- def.configKey(),
- "refreshAfterWrite",
- toSeconds(refreshAfterWrite),
- SECONDS),
- SECONDS);
- } else if (refreshAfterWrite != null) {
- builder.refreshAfterWrite(refreshAfterWrite.toNanos(), NANOSECONDS);
- }
-
- return builder;
+ public <K, V> LoadingCache<K, V> build(CacheDef<K, V> def, CacheLoader<K, V> loader) {
+ return CaffeinatedGuava.build(create(def), loader);
}
private <K, V> Caffeine<K, V> create(CacheDef<K, V> def) {
@@ -183,15 +118,6 @@
}
@SuppressWarnings("unchecked")
- private static <K, V> CacheBuilder<K, V> newLegacyCacheBuilder() {
- return (CacheBuilder<K, V>) CacheBuilder.newBuilder();
- }
-
- private static <K, V> com.google.common.cache.Weigher<K, V> unitWeight() {
- return (key, value) -> 1;
- }
-
- @SuppressWarnings("unchecked")
private static <K, V> Caffeine<K, V> newCacheBuilder() {
return (Caffeine<K, V>) Caffeine.newBuilder();
}
diff --git a/java/com/google/gerrit/server/cache/serialize/CacheSerializer.java b/java/com/google/gerrit/server/cache/serialize/CacheSerializer.java
index 5377fc1..bb28a6d 100644
--- a/java/com/google/gerrit/server/cache/serialize/CacheSerializer.java
+++ b/java/com/google/gerrit/server/cache/serialize/CacheSerializer.java
@@ -31,7 +31,7 @@
* @return serializer of type {@code T}.
*/
static <T, D> CacheSerializer<T> convert(CacheSerializer<D> delegate, Converter<T, D> converter) {
- return new CacheSerializer<T>() {
+ return new CacheSerializer<>() {
@Override
public byte[] serialize(T object) {
return delegate.serialize(converter.convert(object));
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/InternalGroupSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/InternalGroupSerializer.java
index 7449917..09f3543 100644
--- a/java/com/google/gerrit/server/cache/serialize/entities/InternalGroupSerializer.java
+++ b/java/com/google/gerrit/server/cache/serialize/entities/InternalGroupSerializer.java
@@ -21,7 +21,7 @@
import com.google.gerrit.entities.InternalGroup;
import com.google.gerrit.server.cache.proto.Cache;
import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
-import java.sql.Timestamp;
+import java.time.Instant;
/** Helper to (de)serialize values for caches. */
public class InternalGroupSerializer {
@@ -33,7 +33,7 @@
.setOwnerGroupUUID(AccountGroup.uuid(proto.getOwnerGroupUuid()))
.setVisibleToAll(proto.getIsVisibleToAll())
.setGroupUUID(AccountGroup.uuid(proto.getGroupUuid()))
- .setCreatedOn(new Timestamp(proto.getCreatedOn()))
+ .setCreatedOn(Instant.ofEpochMilli(proto.getCreatedOn()))
.setMembers(
proto.getMembersIdsList().stream()
.map(a -> Account.id(a))
@@ -62,7 +62,7 @@
.setOwnerGroupUuid(autoValue.getOwnerGroupUUID().get())
.setIsVisibleToAll(autoValue.isVisibleToAll())
.setGroupUuid(autoValue.getGroupUUID().get())
- .setCreatedOn(autoValue.getCreatedOn().getTime());
+ .setCreatedOn(autoValue.getCreatedOn().toEpochMilli());
autoValue.getMembers().stream().forEach(m -> builder.addMembersIds(m.get()));
autoValue.getSubgroups().stream().forEach(s -> builder.addSubgroupUuids(s.get()));
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializer.java
index c00961f..d576499 100644
--- a/java/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializer.java
+++ b/java/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializer.java
@@ -24,6 +24,7 @@
import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.server.cache.proto.Cache;
+import java.util.Optional;
/** Helper to (de)serialize values for caches. */
public class LabelTypeSerializer {
@@ -36,6 +37,7 @@
proto.getValuesList().stream()
.map(LabelValueSerializer::deserialize)
.collect(toImmutableList()))
+ .setDescription(Optional.ofNullable(proto.getDescription()))
.setFunction(FUNCTION_CONVERTER.convert(proto.getFunction()))
.setAllowPostSubmit(proto.getAllowPostSubmit())
.setIgnoreSelfApproval(proto.getIgnoreSelfApproval())
@@ -68,6 +70,7 @@
autoValue.getValues().stream()
.map(LabelValueSerializer::serialize)
.collect(toImmutableList()))
+ .setDescription(autoValue.getDescription().orElse(""))
.setFunction(FUNCTION_CONVERTER.reverse().convert(autoValue.getFunction()))
.setCopyCondition(autoValue.getCopyCondition().orElse(""))
.setCopyAnyScore(autoValue.isCopyAnyScore())
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementExpressionResultSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementExpressionResultSerializer.java
index 4e997b4..436fe76 100644
--- a/java/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementExpressionResultSerializer.java
+++ b/java/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementExpressionResultSerializer.java
@@ -14,10 +14,12 @@
package com.google.gerrit.server.cache.serialize.entities;
+import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.entities.SubmitRequirementExpressionResult;
import com.google.gerrit.server.cache.proto.Cache.SubmitRequirementExpressionResultProto;
+import java.util.Optional;
/**
* Serializer of a {@link SubmitRequirementExpressionResult} to {@link
@@ -30,7 +32,8 @@
SubmitRequirementExpression.create(proto.getExpression()),
SubmitRequirementExpressionResult.Status.valueOf(proto.getStatus()),
proto.getPassingAtomsList().stream().collect(ImmutableList.toImmutableList()),
- proto.getFailingAtomsList().stream().collect(ImmutableList.toImmutableList()));
+ proto.getFailingAtomsList().stream().collect(ImmutableList.toImmutableList()),
+ Optional.ofNullable(Strings.emptyToNull(proto.getErrorMessage())));
}
public static SubmitRequirementExpressionResultProto serialize(
@@ -40,6 +43,7 @@
.setStatus(r.status().name())
.addAllPassingAtoms(r.passingAtoms())
.addAllFailingAtoms(r.failingAtoms())
+ .setErrorMessage(r.errorMessage().orElse(""))
.build();
}
}
diff --git a/java/com/google/gerrit/server/change/AbandonUtil.java b/java/com/google/gerrit/server/change/AbandonUtil.java
index d030ec1..29bd045 100644
--- a/java/com/google/gerrit/server/change/AbandonUtil.java
+++ b/java/com/google/gerrit/server/change/AbandonUtil.java
@@ -99,7 +99,7 @@
msg.append(" ").append(change.getId().get());
}
msg.append(".");
- logger.atSevere().withCause(e).log(msg.toString());
+ logger.atSevere().withCause(e).log("%s", msg);
}
}
logger.atInfo().log("Auto-Abandoned %d of %d changes.", count, changesToAbandon.size());
diff --git a/java/com/google/gerrit/server/change/ActionJson.java b/java/com/google/gerrit/server/change/ActionJson.java
index 54ebf40..63e2c08 100644
--- a/java/com/google/gerrit/server/change/ActionJson.java
+++ b/java/com/google/gerrit/server/change/ActionJson.java
@@ -118,6 +118,10 @@
copy.topic = changeInfo.topic;
copy.attentionSet =
changeInfo.attentionSet == null ? null : ImmutableMap.copyOf(changeInfo.attentionSet);
+ copy.removedFromAttentionSet =
+ changeInfo.removedFromAttentionSet == null
+ ? null
+ : ImmutableMap.copyOf(changeInfo.removedFromAttentionSet);
copy.assignee = changeInfo.assignee;
copy.hashtags = changeInfo.hashtags;
copy.changeId = changeInfo.changeId;
@@ -202,7 +206,7 @@
return out;
}
- private Map<String, ActionInfo> toActionMap(
+ private ImmutableMap<String, ActionInfo> toActionMap(
RevisionResource rsrc,
List<ActionVisitor> visitors,
ChangeInfo changeInfo,
@@ -222,6 +226,6 @@
}
out.put(d.getId(), actionInfo);
}
- return out;
+ return ImmutableMap.copyOf(out);
}
}
diff --git a/java/com/google/gerrit/server/change/AttentionSetEntryResource.java b/java/com/google/gerrit/server/change/AttentionSetEntryResource.java
index 6c6c765..e27a1a7 100644
--- a/java/com/google/gerrit/server/change/AttentionSetEntryResource.java
+++ b/java/com/google/gerrit/server/change/AttentionSetEntryResource.java
@@ -22,7 +22,7 @@
/** REST resource that represents an entry in the attention set of a change. */
public class AttentionSetEntryResource implements RestResource {
public static final TypeLiteral<RestView<AttentionSetEntryResource>> ATTENTION_SET_ENTRY_KIND =
- new TypeLiteral<RestView<AttentionSetEntryResource>>() {};
+ new TypeLiteral<>() {};
public interface Factory {
AttentionSetEntryResource create(ChangeResource change, Account.Id id);
diff --git a/java/com/google/gerrit/server/change/BatchAbandon.java b/java/com/google/gerrit/server/change/BatchAbandon.java
index e0a72ac4..ad6d7fb 100644
--- a/java/com/google/gerrit/server/change/BatchAbandon.java
+++ b/java/com/google/gerrit/server/change/BatchAbandon.java
@@ -20,6 +20,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.config.ChangeCleanupConfig;
+import com.google.gerrit.server.notedb.StoreSubmitRequirementsOp;
import com.google.gerrit.server.plugincontext.PluginItemContext;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.update.BatchUpdate;
@@ -34,15 +35,18 @@
private final AbandonOp.Factory abandonOpFactory;
private final ChangeCleanupConfig cfg;
private final PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore;
+ private final StoreSubmitRequirementsOp.Factory storeSubmitRequirementsOpFactory;
@Inject
BatchAbandon(
AbandonOp.Factory abandonOpFactory,
ChangeCleanupConfig cfg,
- PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore) {
+ PluginItemContext<AccountPatchReviewStore> accountPatchReviewStore,
+ StoreSubmitRequirementsOp.Factory storeSubmitRequirementsOpFactory) {
this.abandonOpFactory = abandonOpFactory;
this.cfg = cfg;
this.accountPatchReviewStore = accountPatchReviewStore;
+ this.storeSubmitRequirementsOpFactory = storeSubmitRequirementsOpFactory;
}
/**
@@ -64,7 +68,7 @@
return;
}
AccountState accountState = user.isIdentifiedUser() ? user.asIdentifiedUser().state() : null;
- try (BatchUpdate u = updateFactory.create(project, user, TimeUtil.nowTs())) {
+ try (BatchUpdate u = updateFactory.create(project, user, TimeUtil.now())) {
u.setNotify(notify);
for (ChangeData change : changes) {
if (!project.equals(change.project())) {
@@ -74,6 +78,9 @@
change.project().get(), project.get()));
}
u.addOp(change.getId(), abandonOpFactory.create(accountState, msgTxt));
+ u.addOp(
+ change.getId(),
+ storeSubmitRequirementsOpFactory.create(change.submitRequirements().values()));
}
u.execute();
diff --git a/java/com/google/gerrit/server/change/ChangeEditResource.java b/java/com/google/gerrit/server/change/ChangeEditResource.java
index 392709e..67edb56 100644
--- a/java/com/google/gerrit/server/change/ChangeEditResource.java
+++ b/java/com/google/gerrit/server/change/ChangeEditResource.java
@@ -31,7 +31,7 @@
*/
public class ChangeEditResource implements RestResource {
public static final TypeLiteral<RestView<ChangeEditResource>> CHANGE_EDIT_KIND =
- new TypeLiteral<RestView<ChangeEditResource>>() {};
+ new TypeLiteral<>() {};
private final ChangeResource change;
private final ChangeEdit edit;
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index 85482e4..9e8d879 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -364,7 +364,7 @@
* <p>Should not be used in new code, as it doesn't result in a single atomic batch ref update for
* code and NoteDb meta refs.
*
- * @param updateRef whether to update the ref during {@code updateRepo}.
+ * @param updateRef whether to update the ref during {@link #updateRepo(RepoContext)}.
*/
@Deprecated
public ChangeInserter setUpdateRef(boolean updateRef) {
@@ -497,15 +497,15 @@
emailSender.setPatchSet(patchSet, patchSetInfo);
emailSender.setNotify(notify);
emailSender.addReviewers(
- reviewerAdditions.flattenResults(AddReviewersOp.Result::addedReviewers).stream()
+ reviewerAdditions.flattenResults(ReviewerOp.Result::addedReviewers).stream()
.map(PatchSetApproval::accountId)
.collect(toImmutableSet()));
emailSender.addReviewersByEmail(
- reviewerAdditions.flattenResults(AddReviewersOp.Result::addedReviewersByEmail));
+ reviewerAdditions.flattenResults(ReviewerOp.Result::addedReviewersByEmail));
emailSender.addExtraCC(
- reviewerAdditions.flattenResults(AddReviewersOp.Result::addedCCs));
+ reviewerAdditions.flattenResults(ReviewerOp.Result::addedCCs));
emailSender.addExtraCCByEmail(
- reviewerAdditions.flattenResults(AddReviewersOp.Result::addedCCsByEmail));
+ reviewerAdditions.flattenResults(ReviewerOp.Result::addedCCsByEmail));
emailSender.setMessageId(
messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSet.id()));
emailSender.send();
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 328c5de..674fc65 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -35,11 +35,13 @@
import static com.google.gerrit.extensions.client.ListChangesOption.TRACKING_IDS;
import static com.google.gerrit.server.ChangeMessagesUtil.createChangeMessageInfo;
import static com.google.gerrit.server.util.AttentionSetUtil.additionsOnly;
+import static com.google.gerrit.server.util.AttentionSetUtil.removalsOnly;
import static java.util.stream.Collectors.toList;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@@ -99,8 +101,6 @@
import com.google.gerrit.server.cancellation.RequestCancelledException;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.TrackingFooters;
-import com.google.gerrit.server.experiments.ExperimentFeatures;
-import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
@@ -255,7 +255,6 @@
TrackingFooters trackingFooters,
Metrics metrics,
RevisionJson.Factory revisionJsonFactory,
- ExperimentFeatures experimentFeatures,
@GerritServerConfig Config cfg,
@Assisted Iterable<ListChangesOption> options,
@Assisted Optional<PluginDefinedInfosFactory> pluginDefinedInfosFactory) {
@@ -274,9 +273,7 @@
this.revisionJson = revisionJsonFactory.create(options);
this.options = Sets.immutableEnumSet(options);
this.includeMergeable = MergeabilityComputationBehavior.fromConfig(cfg).includeInApi();
- this.lazyLoad =
- containsAnyOf(this.options, REQUIRE_LAZY_LOAD)
- || lazyloadSubmitRequirements(this.options, experimentFeatures);
+ this.lazyLoad = containsAnyOf(this.options, REQUIRE_LAZY_LOAD);
this.pluginDefinedInfosFactory = pluginDefinedInfosFactory;
logger.atFine().log("options = %s", options);
@@ -556,8 +553,8 @@
info.subject = c.getSubject();
info.status = c.getStatus().asChangeStatus();
info.owner = new AccountInfo(c.getOwner().get());
- info.created = c.getCreatedOn();
- info.updated = c.getLastUpdatedOn();
+ info.setCreated(c.getCreatedOn());
+ info.setUpdated(c.getLastUpdatedOn());
info._number = c.getId().get();
info.problems = result.problems();
info.isPrivate = c.isPrivate() ? true : null;
@@ -603,6 +600,12 @@
out.branch = in.getDest().shortName();
out.topic = in.getTopic();
if (!cd.attentionSet().isEmpty()) {
+ out.removedFromAttentionSet =
+ removalsOnly(cd.attentionSet()).stream()
+ .collect(
+ toImmutableMap(
+ a -> a.account().get(),
+ a -> AttentionSetUtil.createAttentionSetInfo(a, accountLoader)));
out.attentionSet =
// This filtering should match GetAttentionSet.
additionsOnly(cd.attentionSet()).stream()
@@ -639,8 +642,8 @@
out.subject = in.getSubject();
out.status = in.getStatus().asChangeStatus();
out.owner = accountLoader.get(in.getOwner());
- out.created = in.getCreatedOn();
- out.updated = in.getLastUpdatedOn();
+ out.setCreated(in.getCreatedOn());
+ out.setUpdated(in.getLastUpdatedOn());
out._number = in.getId().get();
out.totalCommentCount = cd.totalCommentCount();
out.unresolvedCommentCount = cd.unresolvedCommentCount();
@@ -772,17 +775,25 @@
List<ReviewerStatusUpdate> reviewerUpdates = cd.reviewerUpdates();
List<ReviewerUpdateInfo> result = new ArrayList<>(reviewerUpdates.size());
for (ReviewerStatusUpdate c : reviewerUpdates) {
- ReviewerUpdateInfo change = new ReviewerUpdateInfo();
- change.updated = c.date();
- change.state = c.state().asReviewerState();
- change.updatedBy = accountLoader.get(c.updatedBy());
- change.reviewer = accountLoader.get(c.reviewer());
+ ReviewerUpdateInfo change =
+ new ReviewerUpdateInfo(
+ c.date(),
+ accountLoader.get(c.updatedBy()),
+ accountLoader.get(c.reviewer()),
+ c.state().asReviewerState());
result.add(change);
}
return result;
}
private boolean submittable(ChangeData cd) {
+ // TODO(ghareeb): Remove the lazy load check after upgrading the change index in google
+ // The ChangeData's submit requirements field is populated from the change index field
+ // "full_submit_requirements" which does not exist in google's change index schema definition
+ // yet.
+ if (lazyLoad) {
+ return cd.submitRequirements().values().stream().allMatch(SubmitRequirementResult::fulfilled);
+ }
return SubmitRecord.allRecordsOK(cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT));
}
@@ -791,21 +802,20 @@
if (!s.isPresent()) {
return;
}
- out.submitted = s.get().granted();
- out.submitter = accountLoader.get(s.get().accountId());
+ out.setSubmitted(s.get().granted(), accountLoader.get(s.get().accountId()));
}
- private Collection<ChangeMessageInfo> messages(ChangeData cd) {
+ private ImmutableList<ChangeMessageInfo> messages(ChangeData cd) {
List<ChangeMessage> messages = cmUtil.byChange(cd.notes());
if (messages.isEmpty()) {
- return Collections.emptyList();
+ return ImmutableList.of();
}
List<ChangeMessageInfo> result = Lists.newArrayListWithCapacity(messages.size());
for (ChangeMessage message : messages) {
result.add(createChangeMessageInfo(message, accountLoader));
}
- return result;
+ return ImmutableList.copyOf(result);
}
private Collection<AccountInfo> removableReviewers(ChangeData cd, ChangeInfo out)
@@ -943,20 +953,4 @@
}
return ImmutableListMultimap.of();
}
-
- private static boolean lazyloadSubmitRequirements(
- Set<ListChangesOption> changeOptions, ExperimentFeatures experimentFeatures) {
- // TODO(ghareeb,hiesel): Remove this method.
- // We are testing the new submit requirements with users in lieu of upgrading the change index
- // to a version that supports the new requirements.
- // Upgrading now, before the feature is finalized would be counter productive, because the index
- // format might change while we iterate over the feature.
- // Allowing changes to lazyload parameters will slow down dashboards for users who have this
- // feature enabled, but will backfill submit requirements that weren't loaded from the index by
- // simply computing them.
- return changeOptions.contains(SUBMIT_REQUIREMENTS)
- && experimentFeatures.isFeatureEnabled(
- ExperimentFeaturesConstants
- .GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS_BACKFILLING_ON_DASHBOARD);
- }
}
diff --git a/java/com/google/gerrit/server/change/ChangeMessageResource.java b/java/com/google/gerrit/server/change/ChangeMessageResource.java
index 25f952d..751faa0 100644
--- a/java/com/google/gerrit/server/change/ChangeMessageResource.java
+++ b/java/com/google/gerrit/server/change/ChangeMessageResource.java
@@ -23,7 +23,7 @@
/** A change message resource. */
public class ChangeMessageResource implements RestResource {
public static final TypeLiteral<RestView<ChangeMessageResource>> CHANGE_MESSAGE_KIND =
- new TypeLiteral<RestView<ChangeMessageResource>>() {};
+ new TypeLiteral<>() {};
private final ChangeResource changeResource;
private final ChangeMessageInfo changeMessage;
diff --git a/java/com/google/gerrit/server/change/ChangeResource.java b/java/com/google/gerrit/server/change/ChangeResource.java
index 970f1b5..919586e 100644
--- a/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/java/com/google/gerrit/server/change/ChangeResource.java
@@ -62,8 +62,7 @@
*/
public static final int JSON_FORMAT_VERSION = 1;
- public static final TypeLiteral<RestView<ChangeResource>> CHANGE_KIND =
- new TypeLiteral<RestView<ChangeResource>>() {};
+ public static final TypeLiteral<RestView<ChangeResource>> CHANGE_KIND = new TypeLiteral<>() {};
public interface Factory {
ChangeResource create(ChangeNotes notes, CurrentUser user);
@@ -166,7 +165,7 @@
// unrelated to the UI.
public void prepareETag(Hasher h, CurrentUser user) {
h.putInt(JSON_FORMAT_VERSION)
- .putLong(getChange().getLastUpdatedOn().getTime())
+ .putLong(getChange().getLastUpdatedOn().toEpochMilli())
.putInt(user.isIdentifiedUser() ? user.getAccountId().get() : 0);
if (user.isIdentifiedUser()) {
diff --git a/java/com/google/gerrit/server/change/ConsistencyChecker.java b/java/com/google/gerrit/server/change/ConsistencyChecker.java
index 7d0bda1..0775647 100644
--- a/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -95,7 +95,7 @@
public abstract static class Result {
private static Result create(ChangeNotes notes, List<ProblemInfo> problems) {
return new AutoValue_ConsistencyChecker_Result(
- notes.getChangeId(), notes.getChange(), problems);
+ notes.getChangeId(), notes.getChange(), ImmutableList.copyOf(problems));
}
public abstract Change.Id id();
@@ -103,7 +103,7 @@
@Nullable
public abstract Change change();
- public abstract List<ProblemInfo> problems();
+ public abstract ImmutableList<ProblemInfo> problems();
}
private final ChangeNotes.Factory notesFactory;
@@ -626,7 +626,7 @@
}
private BatchUpdate newBatchUpdate() {
- return updateFactory.create(change().getProject(), user.get(), TimeUtil.nowTs());
+ return updateFactory.create(change().getProject(), user.get(), TimeUtil.now());
}
private void fixPatchSetRef(ProblemInfo p, PatchSet ps) {
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerOp.java b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
index 1e40429..0116b01 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -195,7 +195,12 @@
try {
if (notify.shouldNotify()) {
emailReviewers(
- ctx.getProject(), currChange, mailMessage, ctx.getWhen(), notify, ctx.getRepoView());
+ ctx.getProject(),
+ currChange,
+ mailMessage,
+ Timestamp.from(ctx.getWhen()),
+ notify,
+ ctx.getRepoView());
}
} catch (Exception err) {
logger.atSevere().withCause(err).log(
@@ -223,7 +228,7 @@
private Iterable<PatchSetApproval> approvals(ChangeContext ctx, Account.Id accountId) {
Iterable<PatchSetApproval> approvals;
- approvals = approvalsUtil.byChange(ctx.getNotes()).values();
+ approvals = ctx.getNotes().getApprovalsWithCopied().values();
return Iterables.filter(approvals, psa -> accountId.equals(psa.accountId()));
}
@@ -251,7 +256,7 @@
deleteReviewerSenderFactory.create(projectName, change.getId());
emailSender.setFrom(userId);
emailSender.addReviewers(Collections.singleton(reviewer.id()));
- emailSender.setChangeMessage(mailMessage, timestamp);
+ emailSender.setChangeMessage(mailMessage, timestamp.toInstant());
emailSender.setNotify(notify);
emailSender.setMessageId(
messageIdGenerator.fromChangeUpdate(repoView, change.currentPatchSetId()));
diff --git a/java/com/google/gerrit/server/change/DraftCommentResource.java b/java/com/google/gerrit/server/change/DraftCommentResource.java
index 19a495d..2e40f2c 100644
--- a/java/com/google/gerrit/server/change/DraftCommentResource.java
+++ b/java/com/google/gerrit/server/change/DraftCommentResource.java
@@ -25,7 +25,7 @@
public class DraftCommentResource implements RestResource {
public static final TypeLiteral<RestView<DraftCommentResource>> DRAFT_COMMENT_KIND =
- new TypeLiteral<RestView<DraftCommentResource>>() {};
+ new TypeLiteral<>() {};
private final RevisionResource rev;
private final HumanComment comment;
diff --git a/java/com/google/gerrit/server/change/EmailReviewComments.java b/java/com/google/gerrit/server/change/EmailReviewComments.java
index 3c7ea44..f94e592 100644
--- a/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -33,7 +33,7 @@
import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
@@ -66,7 +66,7 @@
PatchSet patchSet,
IdentifiedUser user,
@Assisted("message") String message,
- Timestamp timestamp,
+ Instant timestamp,
List<? extends Comment> comments,
@Assisted("patchSetComment") String patchSetComment,
List<LabelVote> labels,
@@ -84,7 +84,7 @@
private final PatchSet patchSet;
private final IdentifiedUser user;
private final String message;
- private final Timestamp timestamp;
+ private final Instant timestamp;
private final List<? extends Comment> comments;
private final String patchSetComment;
private final List<LabelVote> labels;
@@ -102,7 +102,7 @@
@Assisted PatchSet patchSet,
@Assisted IdentifiedUser user,
@Assisted("message") String message,
- @Assisted Timestamp timestamp,
+ @Assisted Instant timestamp,
@Assisted List<? extends Comment> comments,
@Nullable @Assisted("patchSetComment") String patchSetComment,
@Assisted List<LabelVote> labels,
diff --git a/java/com/google/gerrit/server/change/FileInfoJsonImpl.java b/java/com/google/gerrit/server/change/FileInfoJsonImpl.java
index b729c11..44b4ded 100644
--- a/java/com/google/gerrit/server/change/FileInfoJsonImpl.java
+++ b/java/com/google/gerrit/server/change/FileInfoJsonImpl.java
@@ -23,6 +23,7 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
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.FilePathAdapter;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.patch.filediff.FileDiffOutput;
@@ -51,9 +52,11 @@
// single-parent commits, or the auto-merge otherwise
return asFileInfo(
diffs.listModifiedFilesAgainstParent(
- change.getProject(), objectId, /* parentNum= */ 0));
+ change.getProject(), objectId, /* parentNum= */ 0, DiffOptions.DEFAULTS));
}
- return asFileInfo(diffs.listModifiedFiles(change.getProject(), base.commitId(), objectId));
+ return asFileInfo(
+ diffs.listModifiedFiles(
+ change.getProject(), base.commitId(), objectId, DiffOptions.DEFAULTS));
} catch (DiffNotAvailableException e) {
convertException(e);
return null; // unreachable. handleAndThrow will throw an exception anyway
@@ -66,7 +69,7 @@
throws ResourceConflictException, PatchListNotAvailableException {
try {
Map<String, FileDiffOutput> modifiedFiles =
- diffs.listModifiedFilesAgainstParent(project, objectId, parent);
+ diffs.listModifiedFilesAgainstParent(project, objectId, parent, DiffOptions.DEFAULTS);
return asFileInfo(modifiedFiles);
} catch (DiffNotAvailableException e) {
convertException(e);
diff --git a/java/com/google/gerrit/server/change/FileResource.java b/java/com/google/gerrit/server/change/FileResource.java
index 5402338..22fe39c 100644
--- a/java/com/google/gerrit/server/change/FileResource.java
+++ b/java/com/google/gerrit/server/change/FileResource.java
@@ -21,8 +21,7 @@
import com.google.inject.TypeLiteral;
public class FileResource implements RestResource {
- public static final TypeLiteral<RestView<FileResource>> FILE_KIND =
- new TypeLiteral<RestView<FileResource>>() {};
+ public static final TypeLiteral<RestView<FileResource>> FILE_KIND = new TypeLiteral<>() {};
private final RevisionResource rev;
private final Patch.Key key;
diff --git a/java/com/google/gerrit/server/change/FixResource.java b/java/com/google/gerrit/server/change/FixResource.java
index b6b5894..0c5ee23 100644
--- a/java/com/google/gerrit/server/change/FixResource.java
+++ b/java/com/google/gerrit/server/change/FixResource.java
@@ -21,8 +21,7 @@
import java.util.List;
public class FixResource implements RestResource {
- public static final TypeLiteral<RestView<FixResource>> FIX_KIND =
- new TypeLiteral<RestView<FixResource>>() {};
+ public static final TypeLiteral<RestView<FixResource>> FIX_KIND = new TypeLiteral<>() {};
private final List<FixReplacement> fixReplacements;
private final RevisionResource revisionResource;
diff --git a/java/com/google/gerrit/server/change/HumanCommentResource.java b/java/com/google/gerrit/server/change/HumanCommentResource.java
index 1611aaa..93a0698 100644
--- a/java/com/google/gerrit/server/change/HumanCommentResource.java
+++ b/java/com/google/gerrit/server/change/HumanCommentResource.java
@@ -23,7 +23,7 @@
public class HumanCommentResource implements RestResource {
public static final TypeLiteral<RestView<HumanCommentResource>> COMMENT_KIND =
- new TypeLiteral<RestView<HumanCommentResource>>() {};
+ new TypeLiteral<>() {};
private final RevisionResource rev;
private final HumanComment comment;
diff --git a/java/com/google/gerrit/server/change/IncludedInRefs.java b/java/com/google/gerrit/server/change/IncludedInRefs.java
index f069251..17a0c9d 100644
--- a/java/com/google/gerrit/server/change/IncludedInRefs.java
+++ b/java/com/google/gerrit/server/change/IncludedInRefs.java
@@ -16,11 +16,8 @@
import static java.util.stream.Collectors.toSet;
+import com.google.common.collect.ImmutableMap;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.extensions.restapi.AuthException;
-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.server.git.GitRepositoryManager;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
@@ -28,7 +25,6 @@
import com.google.inject.Inject;
import java.io.IOException;
import java.util.Collection;
-import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -55,8 +51,7 @@
public Map<String, Set<String>> apply(
Project.NameKey project, Set<String> commits, Set<String> refNames)
- throws ResourceConflictException, BadRequestException, IOException,
- PermissionBackendException, ResourceNotFoundException, AuthException {
+ throws IOException, PermissionBackendException {
try (Repository repo = repoManager.openRepository(project)) {
Set<Ref> visibleRefs = getVisibleRefs(repo, refNames, project);
@@ -72,7 +67,7 @@
}
}
}
- return Collections.EMPTY_MAP;
+ return ImmutableMap.of();
}
private Set<Ref> getVisibleRefs(Repository repo, Set<String> refNames, Project.NameKey project)
diff --git a/java/com/google/gerrit/server/change/LabelsJson.java b/java/com/google/gerrit/server/change/LabelsJson.java
index 5ce121b..325b20a 100644
--- a/java/com/google/gerrit/server/change/LabelsJson.java
+++ b/java/com/google/gerrit/server/change/LabelsJson.java
@@ -48,7 +48,7 @@
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import com.google.inject.Singleton;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -95,53 +95,44 @@
return ImmutableMap.copyOf(Maps.transformValues(withStatus, LabelWithStatus::label));
}
- /** Returns all labels that the provided user has permission to vote on. */
+ /**
+ * Returns A map of all label names and the values that the provided user has permission to vote
+ * on.
+ *
+ * @param filterApprovalsBy a Gerrit user ID.
+ * @param cd {@link ChangeData} corresponding to a specific gerrit change.
+ * @return A Map where the key contain a label name, and the value is a list of the permissible
+ * vote values that the user can vote on.
+ */
Map<String, Collection<String>> permittedLabels(Account.Id filterApprovalsBy, ChangeData cd)
throws PermissionBackendException {
- boolean isMerged = cd.change().isMerged();
- LabelTypes labelTypes = cd.getLabelTypes();
- Map<String, LabelType> toCheck = new HashMap<>();
- for (SubmitRecord rec : submitRecords(cd)) {
- if (rec.labels != null) {
- for (SubmitRecord.Label r : rec.labels) {
- Optional<LabelType> type = labelTypes.byLabel(r.label);
- if (type.isPresent() && (!isMerged || type.get().isAllowPostSubmit())) {
- toCheck.put(type.get().getName(), type.get());
- }
- }
- }
- }
-
- Map<String, Short> labels = null;
- Set<LabelPermission.WithValue> can =
- permissionBackend.absentUser(filterApprovalsBy).change(cd).testLabels(toCheck.values());
SetMultimap<String, String> permitted = LinkedHashMultimap.create();
- for (SubmitRecord rec : submitRecords(cd)) {
- if (rec.labels == null) {
+ boolean isMerged = cd.change().isMerged();
+ Map<String, Short> currentUserVotes = currentLabels(filterApprovalsBy, cd);
+ for (LabelType labelType : cd.getLabelTypes().getLabelTypes()) {
+ if (isMerged && !labelType.isAllowPostSubmit()) {
continue;
}
- for (SubmitRecord.Label r : rec.labels) {
- Optional<LabelType> type = labelTypes.byLabel(r.label);
- if (!type.isPresent() || (isMerged && !type.get().isAllowPostSubmit())) {
- continue;
+ Set<LabelPermission.WithValue> can =
+ permissionBackend.absentUser(filterApprovalsBy).change(cd).test(labelType);
+ for (LabelValue v : labelType.getValues()) {
+ boolean ok = can.contains(new LabelPermission.WithValue(labelType, v));
+ if (isMerged) {
+ // Votes cannot be decreased if the change is merged. Only accept the label value if it's
+ // greater or equal than the user's latest vote.
+ short prev = currentUserVotes.getOrDefault(labelType.getName(), (short) 0);
+ ok &= v.getValue() >= prev;
}
-
- for (LabelValue v : type.get().getValues()) {
- boolean ok = can.contains(new LabelPermission.WithValue(type.get(), v));
- if (isMerged) {
- if (labels == null) {
- labels = currentLabels(filterApprovalsBy, cd);
- }
- short prev = labels.getOrDefault(type.get().getName(), (short) 0);
- ok &= v.getValue() >= prev;
- }
- if (ok) {
- permitted.put(r.label, v.formatValue());
- }
+ if (ok) {
+ permitted.put(labelType.getName(), v.formatValue());
}
}
}
+ clearOnlyZerosEntries(permitted);
+ return permitted.asMap();
+ }
+ private static void clearOnlyZerosEntries(SetMultimap<String, String> permitted) {
List<String> toClear = Lists.newArrayListWithCapacity(permitted.keySet().size());
for (Map.Entry<String, Collection<String>> e : permitted.asMap().entrySet()) {
if (isOnlyZero(e.getValue())) {
@@ -151,7 +142,6 @@
for (String label : toClear) {
permitted.removeAll(label);
}
- return permitted.asMap();
}
private static boolean isOnlyZero(Collection<String> values) {
@@ -209,10 +199,10 @@
private ApprovalInfo approvalInfo(
AccountLoader accountLoader,
Account.Id id,
- Integer value,
- VotingRangeInfo permittedVotingRange,
- String tag,
- Timestamp date) {
+ @Nullable Integer value,
+ @Nullable VotingRangeInfo permittedVotingRange,
+ @Nullable String tag,
+ @Nullable Instant date) {
ApprovalInfo ai = new ApprovalInfo(id.get(), value, permittedVotingRange, tag, date);
accountLoader.put(ai);
return ai;
@@ -319,7 +309,7 @@
if (info != null) {
info.value = Integer.valueOf(val);
info.permittedVotingRange = pvr.getOrDefault(type.get().getName(), null);
- info.date = psa.granted();
+ info.setDate(psa.granted());
info.tag = psa.tag().orElse(null);
if (psa.postSubmit()) {
info.postSubmit = true;
@@ -368,9 +358,23 @@
}
}
}
+ setLabelsDescription(labels, labelTypes);
return labels;
}
+ private void setLabelsDescription(
+ Map<String, LabelsJson.LabelWithStatus> labels, LabelTypes labelTypes) {
+ for (Map.Entry<String, LabelWithStatus> entry : labels.entrySet()) {
+ String labelName = entry.getKey();
+ Optional<LabelType> type = labelTypes.byLabel(labelName);
+ if (!type.isPresent()) {
+ continue;
+ }
+ LabelWithStatus labelWithStatus = entry.getValue();
+ labelWithStatus.label().description = type.get().getDescription().orElse(null);
+ }
+ }
+
private void setLabelScores(
AccountLoader accountLoader,
LabelType type,
@@ -438,7 +442,7 @@
Integer value;
VotingRangeInfo permittedVotingRange = pvr.getOrDefault(lt.get().getName(), null);
String tag = null;
- Timestamp date = null;
+ Instant date = null;
PatchSetApproval psa = current.get(accountId, lt.get().getName());
if (psa != null) {
value = Integer.valueOf(psa.value());
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index 209901d..aed1774 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -298,10 +298,9 @@
}
}
- // Approvals that are being set in the new patch-set during this operation are not available yet
- // outside of the scope of this method. Only copied approvals are set here.
if (storeCopiedVotes) {
- approvalsUtil.byPatchSet(ctx.getNotes(), patchSet).forEach(a -> update.putCopiedApproval(a));
+ approvalsUtil.persistCopiedApprovals(
+ ctx.getNotes(), patchSet, ctx.getRevWalk(), ctx.getRepoView().getConfig(), update);
}
return true;
diff --git a/java/com/google/gerrit/server/change/RebaseChangeOp.java b/java/com/google/gerrit/server/change/RebaseChangeOp.java
index 3e67cca..57f94ff 100644
--- a/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -392,7 +392,7 @@
if (committerIdent != null) {
cb.setCommitter(committerIdent);
} else {
- cb.setCommitter(ctx.getIdentifiedUser().newCommitterIdent(ctx.getWhen(), ctx.getTimeZone()));
+ cb.setCommitter(ctx.newCommitterIdent());
}
if (matchAuthorToCommitterDate) {
cb.setAuthor(
diff --git a/java/com/google/gerrit/server/change/RelatedChangesSorter.java b/java/com/google/gerrit/server/change/RelatedChangesSorter.java
index 547452e..719578f 100644
--- a/java/com/google/gerrit/server/change/RelatedChangesSorter.java
+++ b/java/com/google/gerrit/server/change/RelatedChangesSorter.java
@@ -29,7 +29,6 @@
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.server.change.RelatedChangesSorter.PatchSetData;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
@@ -196,7 +195,7 @@
List<PatchSetData> start)
throws PermissionBackendException {
if (start.isEmpty()) {
- return ImmutableList.of();
+ return new ArrayList<>();
}
Map<Change.Id, PatchSet.Id> maxPatchSetIds = new HashMap<>();
Set<PatchSetData> seen = new HashSet<>();
diff --git a/java/com/google/gerrit/server/change/ReviewerModifier.java b/java/com/google/gerrit/server/change/ReviewerModifier.java
index fffb107..bfc7841 100644
--- a/java/com/google/gerrit/server/change/ReviewerModifier.java
+++ b/java/com/google/gerrit/server/change/ReviewerModifier.java
@@ -648,7 +648,7 @@
}
public <T> ImmutableSet<T> flattenResults(
- Function<AddReviewersOp.Result, ? extends Collection<T>> func) {
+ Function<ReviewerOp.Result, ? extends Collection<T>> func) {
modifications()
.forEach(
a ->
diff --git a/java/com/google/gerrit/server/change/ReviewerResource.java b/java/com/google/gerrit/server/change/ReviewerResource.java
index 7a98f2b..e688a7b 100644
--- a/java/com/google/gerrit/server/change/ReviewerResource.java
+++ b/java/com/google/gerrit/server/change/ReviewerResource.java
@@ -29,7 +29,7 @@
public class ReviewerResource implements RestResource {
public static final TypeLiteral<RestView<ReviewerResource>> REVIEWER_KIND =
- new TypeLiteral<RestView<ReviewerResource>>() {};
+ new TypeLiteral<>() {};
public interface Factory {
ReviewerResource create(ChangeResource change, Account.Id id);
diff --git a/java/com/google/gerrit/server/change/RevisionJson.java b/java/com/google/gerrit/server/change/RevisionJson.java
index 33f3d4f..5a2a0eb 100644
--- a/java/com/google/gerrit/server/change/RevisionJson.java
+++ b/java/com/google/gerrit/server/change/RevisionJson.java
@@ -285,7 +285,7 @@
out.isCurrent = in.id().equals(c.currentPatchSetId());
out._number = in.id().get();
out.ref = in.refName();
- out.created = in.createdOn();
+ out.setCreated(in.createdOn());
out.uploader = accountLoader.get(in.uploader());
out.fetch = makeFetchMap(cd, in);
out.kind = changeKindCache.getChangeKind(rw, repo != null ? repo.getConfig() : null, cd, in);
diff --git a/java/com/google/gerrit/server/change/RevisionResource.java b/java/com/google/gerrit/server/change/RevisionResource.java
index 30fa593..e5a57b2 100644
--- a/java/com/google/gerrit/server/change/RevisionResource.java
+++ b/java/com/google/gerrit/server/change/RevisionResource.java
@@ -35,7 +35,7 @@
public class RevisionResource implements RestResource, HasETag {
public static final TypeLiteral<RestView<RevisionResource>> REVISION_KIND =
- new TypeLiteral<RestView<RevisionResource>>() {};
+ new TypeLiteral<>() {};
public static RevisionResource createNonCacheable(ChangeResource change, PatchSet ps) {
return new RevisionResource(change, ps, Optional.empty(), false);
diff --git a/java/com/google/gerrit/server/change/RobotCommentResource.java b/java/com/google/gerrit/server/change/RobotCommentResource.java
index b12727d..3662575 100644
--- a/java/com/google/gerrit/server/change/RobotCommentResource.java
+++ b/java/com/google/gerrit/server/change/RobotCommentResource.java
@@ -23,7 +23,7 @@
public class RobotCommentResource implements RestResource {
public static final TypeLiteral<RestView<RobotCommentResource>> ROBOT_COMMENT_KIND =
- new TypeLiteral<RestView<RobotCommentResource>>() {};
+ new TypeLiteral<>() {};
private final RevisionResource rev;
private final RobotComment comment;
diff --git a/java/com/google/gerrit/server/change/SubmitRequirementsJson.java b/java/com/google/gerrit/server/change/SubmitRequirementsJson.java
index 8eeec62..31d8a15 100644
--- a/java/com/google/gerrit/server/change/SubmitRequirementsJson.java
+++ b/java/com/google/gerrit/server/change/SubmitRequirementsJson.java
@@ -36,28 +36,37 @@
if (req.applicabilityExpression().isPresent()) {
info.applicabilityExpressionResult =
submitRequirementExpressionToInfo(
- req.applicabilityExpression().get(), result.applicabilityExpressionResult().get());
+ req.applicabilityExpression().get(),
+ result.applicabilityExpressionResult().get(),
+ /* hide= */ true); // Always hide applicability expressions on the API
}
if (req.overrideExpression().isPresent()) {
info.overrideExpressionResult =
submitRequirementExpressionToInfo(
- req.overrideExpression().get(), result.overrideExpressionResult().get());
+ req.overrideExpression().get(),
+ result.overrideExpressionResult().get(),
+ /* hide= */ false);
}
info.submittabilityExpressionResult =
submitRequirementExpressionToInfo(
- req.submittabilityExpression(), result.submittabilityExpressionResult());
+ req.submittabilityExpression(),
+ result.submittabilityExpressionResult(),
+ /* hide= */ false);
info.status = SubmitRequirementResultInfo.Status.valueOf(result.status().toString());
info.isLegacy = result.isLegacy();
return info;
}
private static SubmitRequirementExpressionInfo submitRequirementExpressionToInfo(
- SubmitRequirementExpression expression, SubmitRequirementExpressionResult result) {
+ SubmitRequirementExpression expression,
+ SubmitRequirementExpressionResult result,
+ boolean hide) {
SubmitRequirementExpressionInfo info = new SubmitRequirementExpressionInfo();
- info.expression = expression.expressionString();
+ info.expression = hide ? null : expression.expressionString();
info.fulfilled = result.status().equals(SubmitRequirementExpressionResult.Status.PASS);
- info.passingAtoms = result.passingAtoms();
- info.failingAtoms = result.failingAtoms();
+ info.passingAtoms = hide ? null : result.passingAtoms();
+ info.failingAtoms = hide ? null : result.failingAtoms();
+ info.errorMessage = result.errorMessage().isPresent() ? result.errorMessage().get() : null;
return info;
}
}
diff --git a/java/com/google/gerrit/server/change/VoteResource.java b/java/com/google/gerrit/server/change/VoteResource.java
index 27b5bec..3f5ec4c 100644
--- a/java/com/google/gerrit/server/change/VoteResource.java
+++ b/java/com/google/gerrit/server/change/VoteResource.java
@@ -19,8 +19,7 @@
import com.google.inject.TypeLiteral;
public class VoteResource implements RestResource {
- public static final TypeLiteral<RestView<VoteResource>> VOTE_KIND =
- new TypeLiteral<RestView<VoteResource>>() {};
+ public static final TypeLiteral<RestView<VoteResource>> VOTE_KIND = new TypeLiteral<>() {};
private final ReviewerResource reviewer;
private final String label;
diff --git a/java/com/google/gerrit/server/change/WalkSorter.java b/java/com/google/gerrit/server/change/WalkSorter.java
index 816a904..44a3d16 100644
--- a/java/com/google/gerrit/server/change/WalkSorter.java
+++ b/java/com/google/gerrit/server/change/WalkSorter.java
@@ -112,8 +112,8 @@
return Iterables.concat(sortedByProject);
}
- private List<PatchSetData> sortProject(Project.NameKey project, Collection<ChangeData> in)
- throws IOException {
+ private ImmutableList<PatchSetData> sortProject(
+ Project.NameKey project, Collection<ChangeData> in) throws IOException {
try (Repository repo = repoManager.openRepository(project);
RevWalk rw = new RevWalk(repo)) {
rw.setRetainBody(retainBody);
@@ -181,7 +181,7 @@
}
}
}
- return result;
+ return ImmutableList.copyOf(result);
}
}
diff --git a/java/com/google/gerrit/server/config/CacheResource.java b/java/com/google/gerrit/server/config/CacheResource.java
index 7a835b1..b2f790c 100644
--- a/java/com/google/gerrit/server/config/CacheResource.java
+++ b/java/com/google/gerrit/server/config/CacheResource.java
@@ -21,8 +21,7 @@
import com.google.inject.TypeLiteral;
public class CacheResource extends ConfigResource {
- public static final TypeLiteral<RestView<CacheResource>> CACHE_KIND =
- new TypeLiteral<RestView<CacheResource>>() {};
+ public static final TypeLiteral<RestView<CacheResource>> CACHE_KIND = new TypeLiteral<>() {};
private final String name;
private final Provider<Cache<?, ?>> cacheProvider;
diff --git a/java/com/google/gerrit/server/config/CapabilityResource.java b/java/com/google/gerrit/server/config/CapabilityResource.java
index 7e3c87e..5a1977b 100644
--- a/java/com/google/gerrit/server/config/CapabilityResource.java
+++ b/java/com/google/gerrit/server/config/CapabilityResource.java
@@ -19,5 +19,5 @@
public class CapabilityResource extends ConfigResource {
public static final TypeLiteral<RestView<CapabilityResource>> CAPABILITY_KIND =
- new TypeLiteral<RestView<CapabilityResource>>() {};
+ new TypeLiteral<>() {};
}
diff --git a/java/com/google/gerrit/server/config/ConfigResource.java b/java/com/google/gerrit/server/config/ConfigResource.java
index f2b7c8e..93efd19 100644
--- a/java/com/google/gerrit/server/config/ConfigResource.java
+++ b/java/com/google/gerrit/server/config/ConfigResource.java
@@ -21,8 +21,7 @@
import java.util.concurrent.TimeUnit;
public class ConfigResource implements RestResource {
- public static final TypeLiteral<RestView<ConfigResource>> CONFIG_KIND =
- new TypeLiteral<RestView<ConfigResource>>() {};
+ public static final TypeLiteral<RestView<ConfigResource>> CONFIG_KIND = new TypeLiteral<>() {};
/**
* Default cache control that gets set on the 'Cache-Control' header for responses on this
diff --git a/java/com/google/gerrit/server/config/ConfigUtil.java b/java/com/google/gerrit/server/config/ConfigUtil.java
index c44b0fd..5d94255 100644
--- a/java/com/google/gerrit/server/config/ConfigUtil.java
+++ b/java/com/google/gerrit/server/config/ConfigUtil.java
@@ -145,7 +145,7 @@
list.add(getEnum(section, subsection, setting, string, all));
} catch (IllegalArgumentException ex) {
// It's better to ignore a wrongly configured enum, rather than fail to load Gerrit.
- logger.atWarning().log(ex.getMessage());
+ logger.atWarning().log("%s", ex.getMessage());
}
}
}
diff --git a/java/com/google/gerrit/server/config/DownloadConfig.java b/java/com/google/gerrit/server/config/DownloadConfig.java
index 00df1e6..a718fa4 100644
--- a/java/com/google/gerrit/server/config/DownloadConfig.java
+++ b/java/com/google/gerrit/server/config/DownloadConfig.java
@@ -54,7 +54,7 @@
for (String s : allSchemes) {
String core = toCoreScheme(s);
if (core == null) {
- logger.atWarning().log("not a core download scheme: " + s);
+ logger.atWarning().log("not a core download scheme: %s", s);
continue;
}
normalized.add(core);
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 3a7f2b2..7d3ff12 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -16,7 +16,9 @@
import static com.google.inject.Scopes.SINGLETON;
+import com.google.common.base.Ticker;
import com.google.common.cache.Cache;
+import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.api.changes.ActionVisitor;
import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
@@ -106,7 +108,6 @@
import com.google.gerrit.server.account.externalids.ExternalIdCacheModule;
import com.google.gerrit.server.account.externalids.ExternalIdModule;
import com.google.gerrit.server.account.externalids.ExternalIdUpsertPreprocessor;
-import com.google.gerrit.server.approval.ApprovalCacheImpl;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.auth.AuthBackend;
import com.google.gerrit.server.auth.UniversalAuthBackend;
@@ -167,9 +168,8 @@
import com.google.gerrit.server.mail.send.FromAddressGenerator;
import com.google.gerrit.server.mail.send.FromAddressGeneratorProvider;
import com.google.gerrit.server.mail.send.InboundEmailRejectionSender;
-import com.google.gerrit.server.mail.send.MailSoySauceProvider;
+import com.google.gerrit.server.mail.send.MailSoySauceModule;
import com.google.gerrit.server.mail.send.MailSoyTemplateProvider;
-import com.google.gerrit.server.mail.send.MailTemplates;
import com.google.gerrit.server.mime.FileTypeRegistry;
import com.google.gerrit.server.mime.MimeUtilFileTypeRegistry;
import com.google.gerrit.server.notedb.NoteDbModule;
@@ -187,6 +187,7 @@
import com.google.gerrit.server.project.ProjectCacheImpl;
import com.google.gerrit.server.project.ProjectNameLockManager;
import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.SubmitRequirementExpressionsValidator;
import com.google.gerrit.server.project.SubmitRequirementsEvaluatorImpl;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.query.approval.ApprovalModule;
@@ -224,7 +225,7 @@
import com.google.inject.Inject;
import com.google.inject.TypeLiteral;
import com.google.inject.internal.UniqueAnnotations;
-import com.google.template.soy.jbcsrc.api.SoySauce;
+import com.google.inject.multibindings.OptionalBinder;
import java.util.List;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.transport.PostReceiveHook;
@@ -248,7 +249,6 @@
bind(RulesCache.class);
bind(BlameCache.class).to(BlameCacheImpl.class);
install(AccountCacheImpl.module());
- install(ApprovalCacheImpl.module());
install(BatchUpdate.module());
install(ChangeKindCacheImpl.module());
install(ChangeFinder.module());
@@ -286,6 +286,7 @@
install(new FileInfoJsonModule());
install(ThreadLocalRequestContext.module());
install(new ApprovalModule());
+ install(new MailSoySauceModule());
factory(CapabilityCollection.Factory.class);
factory(ChangeData.AssistedFactory.class);
@@ -327,7 +328,6 @@
bind(ApprovalsUtil.class);
- bind(SoySauce.class).annotatedWith(MailTemplates.class).toProvider(MailSoySauceProvider.class);
bind(FromAddressGenerator.class).toProvider(FromAddressGeneratorProvider.class).in(SINGLETON);
bind(Boolean.class)
.annotatedWith(EnablePeerIPInReflogRecord.class)
@@ -337,6 +337,9 @@
bind(PatchSetInfoFactory.class);
bind(IdentifiedUser.GenericFactory.class).in(SINGLETON);
bind(AccountControl.Factory.class);
+ OptionalBinder.newOptionalBinder(binder(), Ticker.class)
+ .setDefault()
+ .toInstance(Ticker.systemTicker());
bind(UiActions.class);
@@ -388,6 +391,8 @@
DynamicSet.bind(binder(), EventListener.class).to(EventsMetrics.class);
DynamicSet.setOf(binder(), UserScopedEventListener.class);
DynamicSet.setOf(binder(), CommitValidationListener.class);
+ DynamicSet.bind(binder(), CommitValidationListener.class)
+ .to(SubmitRequirementExpressionsValidator.class);
DynamicSet.setOf(binder(), CommentValidator.class);
DynamicSet.setOf(binder(), ChangeMessageModifier.class);
DynamicSet.setOf(binder(), RefOperationValidationListener.class);
@@ -430,6 +435,7 @@
DynamicItem.itemOf(binder(), MergeSuperSetComputation.class);
DynamicItem.itemOf(binder(), ProjectNameLockManager.class);
DynamicSet.setOf(binder(), SubmitRule.class);
+ DynamicSet.setOf(binder(), SubmitRequirement.class);
DynamicSet.setOf(binder(), QuotaEnforcer.class);
DynamicSet.setOf(binder(), PerformanceLogger.class);
DynamicSet.bind(binder(), PerformanceLogger.class).to(PerformanceMetrics.class);
diff --git a/java/com/google/gerrit/server/config/SshClientImplementation.java b/java/com/google/gerrit/server/config/SshClientImplementation.java
deleted file mode 100644
index 5811e4d..0000000
--- a/java/com/google/gerrit/server/config/SshClientImplementation.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.config;
-
-import static com.google.common.base.Preconditions.checkArgument;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Enums;
-import com.google.common.base.Strings;
-
-/* SSH implementation to use by JGit SSH client transport protocol. */
-public enum SshClientImplementation {
- /** JCraft JSch implementation. */
- JSCH,
-
- /** Apache MINA implementation. */
- APACHE;
-
- private static final String ENV_VAR = "SSH_CLIENT_IMPLEMENTATION";
- private static final String SYS_PROP = "gerrit.sshClientImplementation";
-
- @VisibleForTesting
- public static SshClientImplementation getFromEnvironment() {
- String value = System.getenv(ENV_VAR);
- if (Strings.isNullOrEmpty(value)) {
- value = System.getProperty(SYS_PROP);
- }
- if (Strings.isNullOrEmpty(value)) {
- return APACHE;
- }
- SshClientImplementation client =
- Enums.getIfPresent(SshClientImplementation.class, value).orNull();
- if (!Strings.isNullOrEmpty(System.getenv(ENV_VAR))) {
- checkArgument(
- client != null, "Invalid value for env variable %s: %s", ENV_VAR, System.getenv(ENV_VAR));
- } else {
- checkArgument(
- client != null,
- "Invalid value for system property %s: %s",
- SYS_PROP,
- System.getProperty(SYS_PROP));
- }
- return client;
- }
-
- public boolean isMina() {
- return this == APACHE;
- }
-}
diff --git a/java/com/google/gerrit/server/config/TaskResource.java b/java/com/google/gerrit/server/config/TaskResource.java
index 7b69533..dac455f 100644
--- a/java/com/google/gerrit/server/config/TaskResource.java
+++ b/java/com/google/gerrit/server/config/TaskResource.java
@@ -19,8 +19,7 @@
import com.google.inject.TypeLiteral;
public class TaskResource extends ConfigResource {
- public static final TypeLiteral<RestView<TaskResource>> TASK_KIND =
- new TypeLiteral<RestView<TaskResource>>() {};
+ public static final TypeLiteral<RestView<TaskResource>> TASK_KIND = new TypeLiteral<>() {};
private final Task<?> task;
diff --git a/java/com/google/gerrit/server/config/TopMenuResource.java b/java/com/google/gerrit/server/config/TopMenuResource.java
index bca6331..f5c71ed 100644
--- a/java/com/google/gerrit/server/config/TopMenuResource.java
+++ b/java/com/google/gerrit/server/config/TopMenuResource.java
@@ -18,6 +18,5 @@
import com.google.inject.TypeLiteral;
public class TopMenuResource extends ConfigResource {
- public static final TypeLiteral<RestView<TopMenuResource>> TOP_MENU_KIND =
- new TypeLiteral<RestView<TopMenuResource>>() {};
+ public static final TypeLiteral<RestView<TopMenuResource>> TOP_MENU_KIND = new TypeLiteral<>() {};
}
diff --git a/java/com/google/gerrit/server/documentation/MarkdownFormatter.java b/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
index d71f83e..307f3c5 100644
--- a/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
+++ b/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
@@ -83,7 +83,7 @@
}
private MutableDataHolder markDownOptions() {
- int options = ALL & ~(HARDWRAPS);
+ int options = ALL & ~HARDWRAPS;
if (suppressHtml) {
options |= SUPPRESS_ALL_HTML;
}
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index bc905c2..81f98e6 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -52,7 +52,7 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -139,7 +139,7 @@
PatchSet currentPatchSet = lookupCurrentPatchSet(notes);
ObjectId patchSetCommitId = currentPatchSet.commitId();
- noteDbEdits.createEdit(repository, notes, currentPatchSet, patchSetCommitId, TimeUtil.nowTs());
+ noteDbEdits.createEdit(repository, notes, currentPatchSet, patchSetCommitId, TimeUtil.now());
}
/**
@@ -187,7 +187,7 @@
RevTree basePatchSetTree = basePatchSetCommit.getTree();
ObjectId newTreeId = merge(repository, changeEdit, basePatchSetTree);
- Timestamp nowTimestamp = TimeUtil.nowTs();
+ Instant nowTimestamp = TimeUtil.now();
String commitMessage = currentEditCommit.getFullMessage();
ObjectId newEditCommitId =
createCommit(repository, basePatchSetCommit, newTreeId, commitMessage, nowTimestamp);
@@ -385,7 +385,7 @@
return unmodifiedEdit.get();
}
- Timestamp nowTimestamp = TimeUtil.nowTs();
+ Instant nowTimestamp = TimeUtil.now();
ObjectId newEditCommit =
createCommit(repository, basePatchsetCommit, newTreeId, newCommitMessage, nowTimestamp);
@@ -501,7 +501,7 @@
RevCommit basePatchsetCommit,
ObjectId tree,
String commitMessage,
- Timestamp timestamp)
+ Instant timestamp)
throws IOException {
try (ObjectInserter objectInserter = repository.newObjectInserter()) {
CommitBuilder builder = new CommitBuilder();
@@ -516,7 +516,7 @@
}
}
- private PersonIdent getCommitterIdent(Timestamp commitTimestamp) {
+ private PersonIdent getCommitterIdent(Instant commitTimestamp) {
IdentifiedUser user = currentUser.get().asIdentifiedUser();
return user.newCommitterIdent(commitTimestamp, tz);
}
@@ -547,7 +547,7 @@
ChangeNotes notes,
PatchSet basePatchSet,
ObjectId newEditCommitId,
- Timestamp timestamp)
+ Instant timestamp)
throws IOException;
}
@@ -647,7 +647,7 @@
ChangeNotes notes,
PatchSet basePatchSet,
ObjectId newEditCommitId,
- Timestamp timestamp)
+ Instant timestamp)
throws IOException {
return noteDbEdits.updateEdit(
notes.getProjectName(), repository, changeEdit, newEditCommitId, timestamp);
@@ -701,7 +701,7 @@
ChangeNotes notes,
PatchSet basePatchSet,
ObjectId newEditCommitId,
- Timestamp timestamp)
+ Instant timestamp)
throws IOException {
return noteDbEdits.createEdit(repository, notes, basePatchSet, newEditCommitId, timestamp);
}
@@ -723,7 +723,7 @@
ChangeNotes notes,
PatchSet basePatchset,
ObjectId newEditCommitId,
- Timestamp timestamp)
+ Instant timestamp)
throws IOException {
Change change = notes.getChange();
String editRefName = getEditRefName(change, basePatchset);
@@ -750,7 +750,7 @@
Repository repository,
ChangeEdit changeEdit,
ObjectId newEditCommitId,
- Timestamp timestamp)
+ Instant timestamp)
throws IOException {
String editRefName = changeEdit.getRefName();
RevCommit currentEditCommit = changeEdit.getEditCommit();
@@ -769,7 +769,7 @@
String refName,
ObjectId currentObjectId,
ObjectId targetObjectId,
- Timestamp timestamp)
+ Instant timestamp)
throws IOException {
RefUpdate ru = repository.updateRef(refName);
ru.setExpectedOldObjectId(currentObjectId);
@@ -795,7 +795,7 @@
PatchSet currentPatchSet,
ObjectId currentEditCommit,
ObjectId newEditCommitId,
- Timestamp nowTimestamp)
+ Instant nowTimestamp)
throws IOException {
String newEditRefName = getEditRefName(changeEdit.getChange(), currentPatchSet);
updateReferenceWithNameChange(
@@ -814,7 +814,7 @@
ObjectId currentObjectId,
String newRefName,
ObjectId targetObjectId,
- Timestamp timestamp)
+ Instant timestamp)
throws IOException {
BatchRefUpdate batchRefUpdate = repository.getRefDatabase().newBatchUpdate();
batchRefUpdate.addCommand(new ReceiveCommand(ObjectId.zeroId(), targetObjectId, newRefName));
@@ -838,7 +838,7 @@
}
}
- private PersonIdent getRefLogIdent(Timestamp timestamp) {
+ private PersonIdent getRefLogIdent(Instant timestamp) {
IdentifiedUser user = currentUser.get().asIdentifiedUser();
return user.newRefLogIdent(timestamp, tz);
}
diff --git a/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index 6b018ce..74834ab 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -185,7 +185,7 @@
message.append("Published edit on patch set ").append(basePatchSet.number()).append(".");
}
- try (BatchUpdate bu = updateFactory.create(change.getProject(), user, TimeUtil.nowTs())) {
+ try (BatchUpdate bu = updateFactory.create(change.getProject(), user, TimeUtil.now())) {
bu.setRepository(repo, rw, oi);
bu.setNotify(notify);
bu.addOp(change.getId(), inserter.setMessage(message.toString()));
diff --git a/java/com/google/gerrit/server/edit/ModificationTarget.java b/java/com/google/gerrit/server/edit/ModificationTarget.java
index 0de0149..86de812 100644
--- a/java/com/google/gerrit/server/edit/ModificationTarget.java
+++ b/java/com/google/gerrit/server/edit/ModificationTarget.java
@@ -52,21 +52,21 @@
/** A specific patchset commit is the target of the modification. */
class PatchsetCommit implements ModificationTarget {
- private final PatchSet patchset;
+ private final PatchSet patchSet;
- PatchsetCommit(PatchSet patchset) {
- this.patchset = patchset;
+ PatchsetCommit(PatchSet patchSet) {
+ this.patchSet = patchSet;
}
@Override
public void ensureTargetMayBeModifiedDespiteExistingEdit(ChangeEdit changeEdit)
throws InvalidChangeOperationException {
- if (!isBasedOn(changeEdit, patchset)) {
+ if (!isBasedOn(changeEdit, patchSet)) {
throw new InvalidChangeOperationException(
String.format(
"Only the patch set %s on which the existing change edit is based may be modified "
+ "(specified patch set: %s)",
- changeEdit.getBasePatchSet().id(), patchset.id()));
+ changeEdit.getBasePatchSet().id(), patchSet.id()));
}
}
@@ -78,7 +78,7 @@
@Override
public void ensureNewEditMayBeBasedOnTarget(Change change)
throws InvalidChangeOperationException {
- PatchSet.Id patchSetId = patchset.id();
+ PatchSet.Id patchSetId = patchSet.id();
PatchSet.Id currentPatchSetId = change.currentPatchSetId();
if (!patchSetId.equals(currentPatchSetId)) {
throw new InvalidChangeOperationException(
@@ -91,13 +91,13 @@
@Override
public RevCommit getCommit(Repository repository) throws IOException {
try (RevWalk revWalk = new RevWalk(repository)) {
- return revWalk.parseCommit(patchset.commitId());
+ return revWalk.parseCommit(patchSet.commitId());
}
}
@Override
public PatchSet getBasePatchset() {
- return patchset;
+ return patchSet;
}
}
diff --git a/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java b/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
index 39ab041..9c0b92a 100644
--- a/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
+++ b/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
@@ -98,7 +98,7 @@
} catch (IOException e) {
String message =
String.format("Could not change the content of %s", dirCacheEntry.getPathString());
- logger.atSevere().withCause(e).log(message);
+ logger.atSevere().withCause(e).log("%s", message);
} catch (InvalidObjectIdException e) {
logger.atSevere().withCause(e).log("Invalid object id in submodule link");
}
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index a7fea3c..2b402a6 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -58,6 +58,7 @@
import com.google.gerrit.server.notedb.ChangeNotes;
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.FilePathAdapter;
import com.google.gerrit.server.patch.filediff.FileDiffOutput;
import com.google.gerrit.server.query.change.ChangeData;
@@ -138,7 +139,7 @@
a.owner = asAccountAttribute(change.getOwner());
a.assignee = asAccountAttribute(change.getAssignee());
a.status = change.getStatus();
- a.createdOn = change.getCreatedOn().getTime() / 1000L;
+ a.createdOn = change.getCreatedOn().getEpochSecond();
a.wip = change.isWorkInProgress() ? true : null;
a.isPrivate = change.isPrivate() ? true : null;
a.cherryPickOfChange =
@@ -174,7 +175,7 @@
/** Extend the existing {@link ChangeAttribute} with additional fields. */
public void extend(ChangeAttribute a, Change change) {
- a.lastUpdated = change.getLastUpdatedOn().getTime() / 1000L;
+ a.lastUpdated = change.getLastUpdatedOn().getEpochSecond();
a.open = change.isNew();
}
@@ -400,7 +401,7 @@
try {
Map<String, FileDiffOutput> modifiedFiles =
diffOperations.listModifiedFilesAgainstParent(
- change.getProject(), patchSet.commitId(), /* parentNum= */ 0);
+ change.getProject(), patchSet.commitId(), /* parentNum= */ 0, DiffOptions.DEFAULTS);
for (FileDiffOutput diff : modifiedFiles.values()) {
if (patchSetAttribute.files == null) {
@@ -436,7 +437,7 @@
p.number = patchSet.number();
p.ref = patchSet.refName();
p.uploader = asAccountAttribute(patchSet.uploader());
- p.createdOn = patchSet.createdOn().getTime() / 1000L;
+ p.createdOn = patchSet.createdOn().getEpochSecond();
PatchSet.Id pId = patchSet.id();
try {
p.parents = new ArrayList<>();
@@ -457,7 +458,7 @@
Map<String, FileDiffOutput> modifiedFiles =
diffOperations.listModifiedFilesAgainstParent(
- change.getProject(), patchSet.commitId(), /* parentNum= */ 0);
+ change.getProject(), patchSet.commitId(), /* parentNum= */ 0, DiffOptions.DEFAULTS);
for (FileDiffOutput fileDiff : modifiedFiles.values()) {
p.sizeDeletions += fileDiff.deletions();
p.sizeInsertions += fileDiff.insertions();
@@ -533,7 +534,7 @@
a.type = approval.labelId().get();
a.value = Short.toString(approval.value());
a.by = asAccountAttribute(approval.accountId());
- a.grantedOn = approval.granted().getTime() / 1000L;
+ a.grantedOn = approval.granted().getEpochSecond();
a.oldValue = null;
Optional<LabelType> lt = labelTypes.byLabel(approval.labelId());
@@ -543,7 +544,7 @@
public MessageAttribute asMessageAttribute(ChangeMessage message) {
MessageAttribute a = new MessageAttribute();
- a.timestamp = message.getWrittenOn().getTime() / 1000L;
+ a.timestamp = message.getWrittenOn().getEpochSecond();
a.reviewer =
message.getAuthor() != null
? asAccountAttribute(message.getAuthor())
diff --git a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
index 1486559..fcf184c 100644
--- a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
+++ b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
@@ -30,20 +30,11 @@
"GerritBackendRequestFeature__store_submit_requirements_on_merge";
/**
- * Allow legacy {@link com.google.gerrit.entities.SubmitRecord}s to be converted and returned as
- * submit requirements by the {@link
- * com.google.gerrit.server.project.SubmitRequirementsEvaluator}.
+ * When set, we compute information from All-Users repository if able, instead of computing it
+ * from the change index.
*/
- public static final String GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS =
- "GerritBackendRequestFeature__enable_submit_requirements";
-
- /**
- * Allow SubmitRequirements to be computed freshly on dashboards irrespective of the value we
- * retrieved from the change index.
- */
- public static final String
- GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS_BACKFILLING_ON_DASHBOARD =
- "GerritBackendRequestFeature__enable_submit_requirements_backfilling_on_dashboard";
+ public static final String GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY =
+ "GerritBackendRequestFeature__compute_from_all_users_repository";
/** Features, enabled by default in the current release. */
public static final ImmutableSet<String> DEFAULT_ENABLED_FEATURES =
diff --git a/java/com/google/gerrit/server/extensions/events/AbstractChangeEvent.java b/java/com/google/gerrit/server/extensions/events/AbstractChangeEvent.java
index b7ee043..fde4088 100644
--- a/java/com/google/gerrit/server/extensions/events/AbstractChangeEvent.java
+++ b/java/com/google/gerrit/server/extensions/events/AbstractChangeEvent.java
@@ -18,17 +18,17 @@
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.events.ChangeEvent;
-import java.sql.Timestamp;
+import java.time.Instant;
/** Base class for all change events. */
public abstract class AbstractChangeEvent implements ChangeEvent {
private final ChangeInfo changeInfo;
private final AccountInfo who;
- private final Timestamp when;
+ private final Instant when;
private final NotifyHandling notify;
protected AbstractChangeEvent(
- ChangeInfo change, AccountInfo who, Timestamp when, NotifyHandling notify) {
+ ChangeInfo change, AccountInfo who, Instant when, NotifyHandling notify) {
this.changeInfo = change;
this.who = who;
this.when = when;
@@ -46,7 +46,7 @@
}
@Override
- public Timestamp getWhen() {
+ public Instant getWhen() {
return when;
}
diff --git a/java/com/google/gerrit/server/extensions/events/AbstractRevisionEvent.java b/java/com/google/gerrit/server/extensions/events/AbstractRevisionEvent.java
index 9d4d299..421a5ad 100644
--- a/java/com/google/gerrit/server/extensions/events/AbstractRevisionEvent.java
+++ b/java/com/google/gerrit/server/extensions/events/AbstractRevisionEvent.java
@@ -19,7 +19,7 @@
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.events.RevisionEvent;
-import java.sql.Timestamp;
+import java.time.Instant;
/** Base class for all revision events. */
public abstract class AbstractRevisionEvent extends AbstractChangeEvent implements RevisionEvent {
@@ -30,7 +30,7 @@
ChangeInfo change,
RevisionInfo revision,
AccountInfo who,
- Timestamp when,
+ Instant when,
NotifyHandling notify) {
super(change, who, when, notify);
revisionInfo = revision;
diff --git a/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java b/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
index e31a1b5..8e4d1e2 100644
--- a/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
+++ b/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
@@ -25,7 +25,7 @@
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import com.google.inject.Singleton;
-import java.sql.Timestamp;
+import java.time.Instant;
/** Helper class to fire an event when a user has been set as assignee on a change. */
@Singleton
@@ -42,7 +42,7 @@
}
public void fire(
- ChangeData changeData, AccountState accountState, AccountState oldAssignee, Timestamp when) {
+ ChangeData changeData, AccountState accountState, AccountState oldAssignee, Instant when) {
if (listeners.isEmpty()) {
return;
}
@@ -63,7 +63,7 @@
private static class Event extends AbstractChangeEvent implements AssigneeChangedListener.Event {
private final AccountInfo oldAssignee;
- Event(ChangeInfo change, AccountInfo editor, AccountInfo oldAssignee, Timestamp when) {
+ Event(ChangeInfo change, AccountInfo editor, AccountInfo oldAssignee, Instant when) {
super(change, editor, when, NotifyHandling.ALL);
this.oldAssignee = oldAssignee;
}
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java b/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
index cbe7c6b..ca1a742 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
@@ -32,7 +32,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
/** Helper class to fire an event when a change has been abandoned. */
@Singleton
@@ -53,7 +53,7 @@
PatchSet ps,
AccountState abandoner,
String reason,
- Timestamp when,
+ Instant when,
NotifyHandling notifyHandling) {
if (listeners.isEmpty()) {
return;
@@ -89,7 +89,7 @@
RevisionInfo revision,
AccountInfo abandoner,
String reason,
- Timestamp when,
+ Instant when,
NotifyHandling notifyHandling) {
super(change, revision, abandoner, when, notifyHandling);
this.reason = reason;
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java b/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java
index 23a4583..acca491 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java
@@ -25,7 +25,7 @@
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import com.google.inject.Singleton;
-import java.sql.Timestamp;
+import java.time.Instant;
/** Helper class to fire an event when a change has been deleted. */
@Singleton
@@ -41,7 +41,7 @@
this.util = util;
}
- public void fire(ChangeData changeData, AccountState deleter, Timestamp when) {
+ public void fire(ChangeData changeData, AccountState deleter, Instant when) {
if (listeners.isEmpty()) {
return;
}
@@ -55,7 +55,7 @@
/** Event to be fired when a change has been deleted. */
private static class Event extends AbstractChangeEvent implements ChangeDeletedListener.Event {
- Event(ChangeInfo change, AccountInfo deleter, Timestamp when) {
+ Event(ChangeInfo change, AccountInfo deleter, Instant when) {
super(change, deleter, when, NotifyHandling.ALL);
}
}
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeMerged.java b/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
index e4896df..870d850 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
@@ -32,7 +32,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
/** Helper class to fire an event when a change has been merged. */
@Singleton
@@ -49,11 +49,7 @@
}
public void fire(
- ChangeData changeData,
- PatchSet ps,
- AccountState merger,
- String newRevisionId,
- Timestamp when) {
+ ChangeData changeData, PatchSet ps, AccountState merger, String newRevisionId, Instant when) {
if (listeners.isEmpty()) {
return;
}
@@ -86,7 +82,7 @@
RevisionInfo revision,
AccountInfo merger,
String newRevisionId,
- Timestamp when) {
+ Instant when) {
super(change, revision, merger, when, NotifyHandling.ALL);
this.newRevisionId = newRevisionId;
}
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeRestored.java b/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
index 8bd222a..c71360b 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
@@ -32,7 +32,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
/** Helper class to fire an event when a change has been restored. */
@Singleton
@@ -49,7 +49,7 @@
}
public void fire(
- ChangeData changeData, PatchSet ps, AccountState restorer, String reason, Timestamp when) {
+ ChangeData changeData, PatchSet ps, AccountState restorer, String reason, Instant when) {
if (listeners.isEmpty()) {
return;
}
@@ -83,7 +83,7 @@
RevisionInfo revision,
AccountInfo restorer,
String reason,
- Timestamp when) {
+ Instant when) {
super(change, revision, restorer, when, NotifyHandling.ALL);
this.reason = reason;
}
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeReverted.java b/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
index 4a46eb0..1abbebb 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
@@ -23,7 +23,7 @@
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import com.google.inject.Singleton;
-import java.sql.Timestamp;
+import java.time.Instant;
/** Helper class to fire an event when a change has been reverted. */
@Singleton
@@ -39,7 +39,7 @@
this.util = util;
}
- public void fire(ChangeData changeData, ChangeData revertChangeData, Timestamp when) {
+ public void fire(ChangeData changeData, ChangeData revertChangeData, Instant when) {
if (listeners.isEmpty()) {
return;
}
@@ -55,7 +55,7 @@
private static class Event extends AbstractChangeEvent implements ChangeRevertedListener.Event {
private final ChangeInfo revertChange;
- Event(ChangeInfo change, ChangeInfo revertChange, Timestamp when) {
+ Event(ChangeInfo change, ChangeInfo revertChange, Instant when) {
super(change, revertChange.owner, when, NotifyHandling.ALL);
this.revertChange = revertChange;
}
diff --git a/java/com/google/gerrit/server/extensions/events/CommentAdded.java b/java/com/google/gerrit/server/extensions/events/CommentAdded.java
index 20c54cf..79544f2 100644
--- a/java/com/google/gerrit/server/extensions/events/CommentAdded.java
+++ b/java/com/google/gerrit/server/extensions/events/CommentAdded.java
@@ -33,7 +33,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Map;
/** Helper class to fire an event when a comment or vote has been added to a change. */
@@ -57,7 +57,7 @@
String comment,
Map<String, Short> approvals,
Map<String, Short> oldApprovals,
- Timestamp when) {
+ Instant when) {
if (listeners.isEmpty()) {
return;
}
@@ -97,7 +97,7 @@
String comment,
Map<String, ApprovalInfo> approvals,
Map<String, ApprovalInfo> oldApprovals,
- Timestamp when) {
+ Instant when) {
super(change, revision, author, when, NotifyHandling.ALL);
this.comment = comment;
this.approvals = approvals;
diff --git a/java/com/google/gerrit/server/extensions/events/EventUtil.java b/java/com/google/gerrit/server/extensions/events/EventUtil.java
index f0d038a..45f7ecb 100644
--- a/java/com/google/gerrit/server/extensions/events/EventUtil.java
+++ b/java/com/google/gerrit/server/extensions/events/EventUtil.java
@@ -36,7 +36,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
@@ -111,7 +111,7 @@
}
public Map<String, ApprovalInfo> approvals(
- AccountState accountState, Map<String, Short> approvals, Timestamp ts) {
+ AccountState accountState, Map<String, Short> approvals, Instant ts) {
Map<String, ApprovalInfo> result = new HashMap<>();
for (Map.Entry<String, Short> e : approvals.entrySet()) {
Integer value = e.getValue() != null ? Integer.valueOf(e.getValue()) : null;
diff --git a/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java b/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
index 846257c..e7903a2 100644
--- a/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
+++ b/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
@@ -26,7 +26,7 @@
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import com.google.inject.Singleton;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Collection;
import java.util.Set;
@@ -50,7 +50,7 @@
ImmutableSortedSet<String> hashtags,
Set<String> added,
Set<String> removed,
- Timestamp when) {
+ Instant when) {
if (listeners.isEmpty()) {
return;
}
@@ -82,7 +82,7 @@
Collection<String> updated,
Collection<String> added,
Collection<String> removed,
- Timestamp when) {
+ Instant when) {
super(change, editor, when, NotifyHandling.ALL);
this.updatedHashtags = updated;
this.addedHashtags = added;
diff --git a/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java b/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
index d81068c..c6076fd 100644
--- a/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
+++ b/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
@@ -31,7 +31,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
/** Helper class to fire an event when the private flag of a change has been toggled. */
@Singleton
@@ -47,7 +47,7 @@
this.util = util;
}
- public void fire(ChangeData changeData, PatchSet patchSet, AccountState account, Timestamp when) {
+ public void fire(ChangeData changeData, PatchSet patchSet, AccountState account, Instant when) {
if (listeners.isEmpty()) {
return;
}
@@ -72,7 +72,7 @@
private static class Event extends AbstractRevisionEvent
implements PrivateStateChangedListener.Event {
- protected Event(ChangeInfo change, RevisionInfo revision, AccountInfo who, Timestamp when) {
+ protected Event(ChangeInfo change, RevisionInfo revision, AccountInfo who, Instant when) {
super(change, revision, who, when, NotifyHandling.ALL);
}
}
diff --git a/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java b/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
index ba73ca1..147e372 100644
--- a/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
+++ b/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
@@ -33,7 +33,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.List;
/** Helper class to fire an event when reviewers have been added to a change. */
@@ -55,7 +55,7 @@
PatchSet patchSet,
List<AccountState> reviewers,
AccountState adder,
- Timestamp when) {
+ Instant when) {
if (listeners.isEmpty() || reviewers.isEmpty()) {
return;
}
@@ -89,7 +89,7 @@
RevisionInfo revision,
List<AccountInfo> reviewers,
AccountInfo adder,
- Timestamp when) {
+ Instant when) {
super(change, revision, adder, when, NotifyHandling.ALL);
this.reviewers = reviewers;
}
diff --git a/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java b/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
index 80037bc..5f9179a 100644
--- a/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
+++ b/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
@@ -33,7 +33,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Map;
/** Helper class to fire an event when a reviewer has been deleted from a change. */
@@ -59,7 +59,7 @@
Map<String, Short> newApprovals,
Map<String, Short> oldApprovals,
NotifyHandling notify,
- Timestamp when) {
+ Instant when) {
if (listeners.isEmpty()) {
return;
}
@@ -104,7 +104,7 @@
Map<String, ApprovalInfo> newApprovals,
Map<String, ApprovalInfo> oldApprovals,
NotifyHandling notify,
- Timestamp when) {
+ Instant when) {
super(change, revision, remover, when, notify);
this.reviewer = reviewer;
this.comment = comment;
diff --git a/java/com/google/gerrit/server/extensions/events/RevisionCreated.java b/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
index 4c78216..a60d982 100644
--- a/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
+++ b/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
@@ -33,7 +33,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
/** Helper class to fire an event when a revision has been created for a change. */
@Singleton
@@ -47,7 +47,7 @@
ChangeData changeData,
PatchSet patchSet,
AccountState uploader,
- Timestamp when,
+ Instant when,
NotifyResolver.Result notify) {}
};
@@ -69,7 +69,7 @@
ChangeData changeData,
PatchSet patchSet,
AccountState uploader,
- Timestamp when,
+ Instant when,
NotifyResolver.Result notify) {
if (listeners.isEmpty()) {
return;
@@ -102,7 +102,7 @@
ChangeInfo change,
RevisionInfo revision,
AccountInfo uploader,
- Timestamp when,
+ Instant when,
NotifyHandling notify) {
super(change, revision, uploader, when, notify);
}
diff --git a/java/com/google/gerrit/server/extensions/events/TopicEdited.java b/java/com/google/gerrit/server/extensions/events/TopicEdited.java
index 08b47f1..008ead5 100644
--- a/java/com/google/gerrit/server/extensions/events/TopicEdited.java
+++ b/java/com/google/gerrit/server/extensions/events/TopicEdited.java
@@ -25,7 +25,7 @@
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import com.google.inject.Singleton;
-import java.sql.Timestamp;
+import java.time.Instant;
/** Helper class to fire an event when the topic of a change has been edited. */
@Singleton
@@ -41,8 +41,7 @@
this.util = util;
}
- public void fire(
- ChangeData changeData, AccountState account, String oldTopicName, Timestamp when) {
+ public void fire(ChangeData changeData, AccountState account, String oldTopicName, Instant when) {
if (listeners.isEmpty()) {
return;
}
@@ -59,7 +58,7 @@
private static class Event extends AbstractChangeEvent implements TopicEditedListener.Event {
private final String oldTopic;
- Event(ChangeInfo change, AccountInfo editor, String oldTopic, Timestamp when) {
+ Event(ChangeInfo change, AccountInfo editor, String oldTopic, Instant when) {
super(change, editor, when, NotifyHandling.ALL);
this.oldTopic = oldTopic;
}
diff --git a/java/com/google/gerrit/server/extensions/events/VoteDeleted.java b/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
index 244e46c..deaaff8 100644
--- a/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
+++ b/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
@@ -33,7 +33,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Map;
/** Helper class to fire an event when a vote has been deleted from a change. */
@@ -59,7 +59,7 @@
NotifyHandling notify,
String message,
AccountState remover,
- Timestamp when) {
+ Instant when) {
if (listeners.isEmpty()) {
return;
}
@@ -103,7 +103,7 @@
NotifyHandling notify,
String message,
AccountInfo remover,
- Timestamp when) {
+ Instant when) {
super(change, revision, remover, when, notify);
this.reviewer = reviewer;
this.approvals = approvals;
diff --git a/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java b/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
index bfc068d..5e20c45 100644
--- a/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
+++ b/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
@@ -31,7 +31,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
/** Helper class to fire an event when the work-in-progress state of a change has been toggled. */
@Singleton
@@ -42,7 +42,7 @@
new WorkInProgressStateChanged() {
@Override
public void fire(
- ChangeData changeData, PatchSet patchSet, AccountState account, Timestamp when) {}
+ ChangeData changeData, PatchSet patchSet, AccountState account, Instant when) {}
};
private final PluginSetContext<WorkInProgressStateChangedListener> listeners;
@@ -60,7 +60,7 @@
this.util = null;
}
- public void fire(ChangeData changeData, PatchSet patchSet, AccountState account, Timestamp when) {
+ public void fire(ChangeData changeData, PatchSet patchSet, AccountState account, Instant when) {
if (listeners.isEmpty()) {
return;
}
@@ -85,7 +85,7 @@
private static class Event extends AbstractRevisionEvent
implements WorkInProgressStateChangedListener.Event {
- protected Event(ChangeInfo change, RevisionInfo revision, AccountInfo who, Timestamp when) {
+ protected Event(ChangeInfo change, RevisionInfo revision, AccountInfo who, Instant when) {
super(change, revision, who, when, NotifyHandling.ALL);
}
}
diff --git a/java/com/google/gerrit/server/git/BanCommit.java b/java/com/google/gerrit/server/git/BanCommit.java
index 242c11b..9cc754c 100644
--- a/java/com/google/gerrit/server/git/BanCommit.java
+++ b/java/com/google/gerrit/server/git/BanCommit.java
@@ -30,7 +30,7 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.Date;
+import java.time.Instant;
import java.util.List;
import java.util.TimeZone;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -155,8 +155,7 @@
}
private PersonIdent createPersonIdent() {
- Date now = new Date();
- return currentUser.get().newCommitterIdent(now, tz);
+ return currentUser.get().newCommitterIdent(Instant.now(), tz);
}
private static String buildCommitMessage(List<ObjectId> bannedCommits, String reason) {
diff --git a/java/com/google/gerrit/server/git/ChangeMessageModifier.java b/java/com/google/gerrit/server/git/ChangeMessageModifier.java
index 580c0b9..3424477 100644
--- a/java/com/google/gerrit/server/git/ChangeMessageModifier.java
+++ b/java/com/google/gerrit/server/git/ChangeMessageModifier.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.git;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.extensions.annotations.ExtensionPoint;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -42,10 +43,13 @@
* @param original the commit of the change being submitted. <b>Note that its commit message may
* be different than newCommitMessage argument.</b>
* @param mergeTip the current HEAD of the destination branch, which will be a parent of a new
- * commit being generated
+ * commit being generated. mergeTip can be null if the destination branch does not yet exist.
* @param destination the branch onto which the change is being submitted
* @return a new not null commit message.
*/
String onSubmit(
- String newCommitMessage, RevCommit original, RevCommit mergeTip, BranchNameKey destination);
+ String newCommitMessage,
+ RevCommit original,
+ @Nullable RevCommit mergeTip,
+ BranchNameKey destination);
}
diff --git a/java/com/google/gerrit/server/git/CodeReviewCommit.java b/java/com/google/gerrit/server/git/CodeReviewCommit.java
index d7538ba..79df21a 100644
--- a/java/com/google/gerrit/server/git/CodeReviewCommit.java
+++ b/java/com/google/gerrit/server/git/CodeReviewCommit.java
@@ -24,6 +24,9 @@
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.submit.CommitMergeStatus;
import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -35,7 +38,10 @@
import org.eclipse.jgit.revwalk.RevWalk;
/** Extended commit entity with code review specific metadata. */
-public class CodeReviewCommit extends RevCommit {
+public class CodeReviewCommit extends RevCommit implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
/**
* Default ordering when merging multiple topologically-equivalent commits.
*
@@ -126,7 +132,7 @@
* Message for the status that is returned to the calling user if the status indicates a problem
* that prevents submit.
*/
- private Optional<String> statusMessage = Optional.empty();
+ private transient Optional<String> statusMessage = Optional.empty();
/** List of files in this commit that contain Git conflict markers. */
private ImmutableSet<String> filesWithGitConflicts;
@@ -191,4 +197,22 @@
public void setNotes(ChangeNotes notes) {
this.notes = notes;
}
+
+ /** Custom serialization due to {@link #statusMessage} not being Serializable by default. */
+ private void writeObject(ObjectOutputStream oos) throws IOException {
+ oos.defaultWriteObject();
+ if (this.statusMessage.isPresent()) {
+ oos.writeUTF(this.statusMessage.get());
+ }
+ }
+
+ /** Custom deserialization due to {@link #statusMessage} not being Serializable by default. */
+ private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
+ ois.defaultReadObject();
+ String statusMessage = null;
+ if (ois.available() > 0) {
+ statusMessage = ois.readUTF();
+ }
+ this.statusMessage = Optional.ofNullable(statusMessage);
+ }
}
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index 73378f6..e52c45f 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -53,8 +53,8 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
import java.text.MessageFormat;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
@@ -146,7 +146,7 @@
* @return ObjectId that represents the newly created commit.
*/
public Change.Id createRevertChange(
- ChangeNotes notes, CurrentUser user, RevertInput input, Timestamp timestamp)
+ ChangeNotes notes, CurrentUser user, RevertInput input, Instant timestamp)
throws RestApiException, UpdateException, ConfigInvalidException, IOException {
String message = Strings.emptyToNull(input.message);
@@ -174,7 +174,7 @@
* @return ObjectId that represents the newly created commit.
*/
public ObjectId createRevertCommit(
- String message, ChangeNotes notes, CurrentUser user, Timestamp ts)
+ String message, ChangeNotes notes, CurrentUser user, Instant ts)
throws RestApiException, IOException {
try (Repository git = repoManager.openRepository(notes.getProjectName());
@@ -206,7 +206,7 @@
String message,
ChangeNotes notes,
CurrentUser user,
- Timestamp ts,
+ Instant ts,
ObjectInserter oi,
RevWalk revWalk,
@Nullable ObjectId generatedChangeId)
@@ -255,7 +255,7 @@
ChangeNotes notes,
CurrentUser user,
@Nullable ObjectId generatedChangeId,
- Timestamp ts,
+ Instant ts,
ObjectInserter oi,
RevWalk revWalk,
Repository git)
diff --git a/java/com/google/gerrit/server/git/DelegateRepository.java b/java/com/google/gerrit/server/git/DelegateRepository.java
index 9ead038..d839bce 100644
--- a/java/com/google/gerrit/server/git/DelegateRepository.java
+++ b/java/com/google/gerrit/server/git/DelegateRepository.java
@@ -214,12 +214,12 @@
}
@Override
- public Set<ObjectId> getAdditionalHaves() {
+ public Set<ObjectId> getAdditionalHaves() throws IOException {
return delegate.getAdditionalHaves();
}
@Override
- public Map<AnyObjectId, Set<Ref>> getAllRefsByPeeledObjectId() {
+ public Map<AnyObjectId, Set<Ref>> getAllRefsByPeeledObjectId() throws IOException {
return delegate.getAllRefsByPeeledObjectId();
}
diff --git a/java/com/google/gerrit/server/git/GarbageCollection.java b/java/com/google/gerrit/server/git/GarbageCollection.java
index 6ae4d62..30330eb 100644
--- a/java/com/google/gerrit/server/git/GarbageCollection.java
+++ b/java/com/google/gerrit/server/git/GarbageCollection.java
@@ -136,7 +136,7 @@
}
b.append(s);
}
- logger.atInfo().log(b.toString());
+ logger.atInfo().log("%s", b);
}
private static void logGcConfiguration(
@@ -153,7 +153,7 @@
}
logGcInfo(projectName, "gc config: " + b.toString());
- logGcInfo(projectName, "pack config: " + (new PackConfig(repo)).toString());
+ logGcInfo(projectName, "pack config: " + new PackConfig(repo).toString());
}
private static String formatConfigValues(Config config, String section, String subsection) {
@@ -176,7 +176,7 @@
print(writer, "failed.\n\n");
StringBuilder b = new StringBuilder();
b.append("[").append(projectName.get()).append("]");
- logger.atSevere().withCause(e).log(b.toString());
+ logger.atSevere().withCause(e).log("%s", b);
}
private static void print(PrintWriter writer, String message) {
diff --git a/java/com/google/gerrit/server/git/GitRepositoryManager.java b/java/com/google/gerrit/server/git/GitRepositoryManager.java
index 8dba3e1..d045baa 100644
--- a/java/com/google/gerrit/server/git/GitRepositoryManager.java
+++ b/java/com/google/gerrit/server/git/GitRepositoryManager.java
@@ -18,7 +18,7 @@
import com.google.inject.ImplementedBy;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.SortedSet;
+import java.util.NavigableSet;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Repository;
@@ -74,7 +74,7 @@
throws RepositoryNotFoundException, RepositoryExistsException, IOException;
/** Returns set of all known projects, sorted by natural NameKey order. */
- SortedSet<Project.NameKey> list();
+ NavigableSet<Project.NameKey> list();
/**
* Check if garbage collection can be performed by the repository manager.
diff --git a/java/com/google/gerrit/server/git/HookUtil.java b/java/com/google/gerrit/server/git/HookUtil.java
index fd29c8deb..cafa18e 100644
--- a/java/com/google/gerrit/server/git/HookUtil.java
+++ b/java/com/google/gerrit/server/git/HookUtil.java
@@ -43,12 +43,12 @@
refs =
rp.getRepository().getRefDatabase().getRefs().stream()
.collect(toMap(Ref::getName, r -> r));
+ rp.setAdvertisedRefs(refs, rp.getAdvertisedObjects());
} catch (ServiceMayNotContinueException e) {
throw e;
} catch (IOException e) {
throw new ServiceMayNotContinueException(e);
}
- rp.setAdvertisedRefs(refs, rp.getAdvertisedObjects());
return refs;
}
@@ -70,12 +70,12 @@
refs =
up.getRepository().getRefDatabase().getRefs().stream()
.collect(toMap(Ref::getName, r -> r));
+ up.setAdvertisedRefs(refs);
} catch (ServiceMayNotContinueException e) {
throw e;
} catch (IOException e) {
throw new ServiceMayNotContinueException(e);
}
- up.setAdvertisedRefs(refs);
return refs;
}
diff --git a/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index 8527ff8..daa2e09 100644
--- a/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
+++ b/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -34,7 +34,7 @@
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
import java.util.EnumSet;
-import java.util.SortedSet;
+import java.util.NavigableSet;
import java.util.TreeSet;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
@@ -254,10 +254,10 @@
}
@Override
- public SortedSet<Project.NameKey> list() {
+ public NavigableSet<Project.NameKey> list() {
ProjectVisitor visitor = new ProjectVisitor(basePath);
scanProjects(visitor);
- return Collections.unmodifiableSortedSet(visitor.found);
+ return Collections.unmodifiableNavigableSet(visitor.found);
}
protected void scanProjects(ProjectVisitor visitor) {
@@ -286,7 +286,7 @@
}
protected class ProjectVisitor extends SimpleFileVisitor<Path> {
- private final SortedSet<Project.NameKey> found = new TreeSet<>();
+ private final NavigableSet<Project.NameKey> found = new TreeSet<>();
private Path startFolder;
public ProjectVisitor(Path startFolder) {
@@ -309,7 +309,7 @@
@Override
public FileVisitResult visitFileFailed(Path file, IOException e) {
- logger.atWarning().log(e.getMessage());
+ logger.atWarning().log("%s", e.getMessage());
return FileVisitResult.CONTINUE;
}
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index c1333cb..d84ce7b 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -31,6 +31,7 @@
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.FooterConstants;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.BranchNameKey;
@@ -630,7 +631,7 @@
* @return new message
*/
public String createCommitMessageOnSubmit(
- RevCommit n, RevCommit mergeTip, ChangeNotes notes, PatchSet.Id id) {
+ RevCommit n, @Nullable RevCommit mergeTip, ChangeNotes notes, PatchSet.Id id) {
return commitMessageGenerator.generate(
n, mergeTip, notes.getChange().getDest(), createDetailedCommitMessage(n, notes, id));
}
@@ -784,8 +785,7 @@
try {
failed(rw, mergeTip, n, getCommitMergeStatus(e.getReason()));
} catch (IOException e2) {
- logger.atSevere().withCause(e2).log("Failed to set merge failure status for " + n.name());
- throw new StorageException("Cannot merge " + n.name(), e);
+ throw new StorageException("Cannot merge " + n.name(), e2);
}
} catch (IOException e) {
throw new StorageException("Cannot merge " + n.name(), e);
diff --git a/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index 4985288..52a34d9 100644
--- a/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -19,6 +19,7 @@
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import com.google.common.base.Strings;
+import com.google.common.base.Ticker;
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.google.gerrit.server.CancellationMetrics;
@@ -240,6 +241,7 @@
private Optional<Long> timeout = Optional.empty();
private final long maxIntervalNanos;
+ private final Ticker ticker;
/**
* Create a new progress monitor for multiple sub-tasks.
@@ -250,10 +252,11 @@
@AssistedInject
private MultiProgressMonitor(
CancellationMetrics cancellationMetrics,
+ Ticker ticker,
@Assisted OutputStream out,
@Assisted TaskKind taskKind,
@Assisted String taskName) {
- this(cancellationMetrics, out, taskKind, taskName, 500, MILLISECONDS);
+ this(cancellationMetrics, ticker, out, taskKind, taskName, 500, MILLISECONDS);
}
/**
@@ -267,12 +270,14 @@
@AssistedInject
private MultiProgressMonitor(
CancellationMetrics cancellationMetrics,
+ Ticker ticker,
@Assisted OutputStream out,
@Assisted TaskKind taskKind,
@Assisted String taskName,
@Assisted long maxIntervalTime,
@Assisted TimeUnit maxIntervalUnit) {
this.cancellationMetrics = cancellationMetrics;
+ this.ticker = ticker;
this.out = out;
this.taskKind = taskKind;
this.taskName = taskName;
@@ -304,7 +309,7 @@
* calls {@link #end()}, the future has an additional {@code maxInterval} to finish before it is
* forcefully cancelled and {@link ExecutionException} is thrown.
*
- * @see #waitForNonFinalTask(Future, long, TimeUnit)
+ * @see #waitForNonFinalTask(Future, long, TimeUnit, long, TimeUnit)
* @param workerFuture a future that returns when worker threads are finished.
* @param taskTimeoutTime overall timeout for the task; the future gets a cancellation signal
* after this timeout is exceeded; non-positive values indicate no timeout.
@@ -345,7 +350,7 @@
/**
* Wait for a non-final task managed by a {@link Future}, with no timeout.
*
- * @see #waitForNonFinalTask(Future, long, TimeUnit)
+ * @see #waitForNonFinalTask(Future, long, TimeUnit, long, TimeUnit)
*/
public <T> T waitForNonFinalTask(Future<T> workerFuture) {
try {
@@ -377,7 +382,7 @@
long cancellationTimeoutTime,
TimeUnit cancellationTimeoutUnit)
throws TimeoutException {
- long overallStart = System.nanoTime();
+ long overallStart = ticker.read();
long cancellationNanos =
cancellationTimeoutTime > 0
? NANOSECONDS.convert(cancellationTimeoutTime, cancellationTimeoutUnit)
@@ -393,16 +398,33 @@
synchronized (this) {
long left = maxIntervalNanos;
while (!workerFuture.isDone() && !done) {
- long start = System.nanoTime();
+ long start = ticker.read();
try {
- NANOSECONDS.timedWait(this, left);
+ // Conditions below gives better granularity for timeouts.
+ // Originally, code always used fixed interval:
+ // NANOSECONDS.timedWait(this, maxIntervalNanos);
+ // As a result, the actual check for timeouts happened only every maxIntervalNanos
+ // (default value 500ms); so even if timout was set to 1ms, the actual timeout was 500ms.
+ // This is not a big issue, however it made our tests for timeouts flaky. For example,
+ // some tests in the CancellationIT set timeout to 1ms and expect that server returns
+ // timeout. However, server often returned OK result, because a request takes less than
+ // 500ms.
+ if (deadlineExceeded || deadline == 0) {
+ // We want to set deadlineExceeded flag as earliest as possible. If it is already
+ // set - there is no reason to wait less than maxIntervalNanos
+ NANOSECONDS.timedWait(this, maxIntervalNanos);
+ } else if (start <= deadline) {
+ // if deadlineExceeded is not set, then we should wait until deadline, but no longer
+ // than maxIntervalNanos (because we want to report a progress every maxIntervalNanos).
+ NANOSECONDS.timedWait(this, Math.min(deadline - start + 1, maxIntervalNanos));
+ }
} catch (InterruptedException e) {
throw new UncheckedExecutionException(e);
}
// Send an update on every wakeup (manual or spurious), but only move
// the spinner every maxInterval.
- long now = System.nanoTime();
+ long now = ticker.read();
if (deadline > 0 && now > deadline) {
if (!deadlineExceeded) {
diff --git a/java/com/google/gerrit/server/git/PerThreadRequestScope.java b/java/com/google/gerrit/server/git/PerThreadRequestScope.java
index b7db542..9b5a674 100644
--- a/java/com/google/gerrit/server/git/PerThreadRequestScope.java
+++ b/java/com/google/gerrit/server/git/PerThreadRequestScope.java
@@ -89,7 +89,7 @@
new Scope() {
@Override
public <T> Provider<T> scope(Key<T> key, Provider<T> creator) {
- return new Provider<T>() {
+ return new Provider<>() {
@Override
public T get() {
return requireContext().get(key, creator);
diff --git a/java/com/google/gerrit/server/git/PluggableCommitMessageGenerator.java b/java/com/google/gerrit/server/git/PluggableCommitMessageGenerator.java
index 804a218..1f5a330 100644
--- a/java/com/google/gerrit/server/git/PluggableCommitMessageGenerator.java
+++ b/java/com/google/gerrit/server/git/PluggableCommitMessageGenerator.java
@@ -18,6 +18,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.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.registration.Extension;
@@ -41,7 +42,10 @@
* modify the message.
*/
public String generate(
- RevCommit original, RevCommit mergeTip, BranchNameKey dest, String originalMessage) {
+ RevCommit original,
+ @Nullable RevCommit mergeTip,
+ BranchNameKey dest,
+ String originalMessage) {
requireNonNull(original.getRawBuffer());
if (mergeTip != null) {
requireNonNull(mergeTip.getRawBuffer());
diff --git a/java/com/google/gerrit/server/git/WorkQueue.java b/java/com/google/gerrit/server/git/WorkQueue.java
index 8b59474..3032bfe 100644
--- a/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/java/com/google/gerrit/server/git/WorkQueue.java
@@ -30,11 +30,10 @@
import com.google.gerrit.server.util.IdGenerator;
import com.google.inject.Inject;
import com.google.inject.Singleton;
-import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.Field;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
@@ -84,10 +83,6 @@
}
}
- private static final UncaughtExceptionHandler LOG_UNCAUGHT_EXCEPTION =
- (t, e) ->
- logger.atSevere().withCause(e).log("WorkQueue thread %s threw exception", t.getName());
-
private final ScheduledExecutorService defaultQueue;
private final IdGenerator idGenerator;
private final MetricMaker metrics;
@@ -152,6 +147,7 @@
* @param threadPriority thread priority.
* @param withMetrics whether to create metrics.
*/
+ @SuppressWarnings("ThreadPriorityCheck")
public ScheduledThreadPoolExecutor createQueue(
int poolsize, String queueName, int threadPriority, boolean withMetrics) {
Executor executor = new Executor(poolsize, queueName);
@@ -259,7 +255,7 @@
public Thread newThread(Runnable task) {
final Thread t = parent.newThread(task);
t.setName(queueName + "-" + tid.getAndIncrement());
- t.setUncaughtExceptionHandler(LOG_UNCAUGHT_EXCEPTION);
+ t.setUncaughtExceptionHandler(WorkQueue::logUncaughtException);
return t;
}
});
@@ -444,6 +440,10 @@
}
}
+ private static void logUncaughtException(Thread t, Throwable e) {
+ logger.atSevere().withCause(e).log("WorkQueue thread %s threw exception", t.getName());
+ }
+
/**
* Runnable needing to know it was canceled. Note that cancel is called only in case the task is
* not in progress already.
@@ -496,7 +496,7 @@
private final Executor executor;
private final int taskId;
private final AtomicBoolean running;
- private final Date startTime;
+ private final Instant startTime;
Task(Runnable runnable, RunnableScheduledFuture<V> task, Executor executor, int taskId) {
this.runnable = runnable;
@@ -504,7 +504,7 @@
this.executor = executor;
this.taskId = taskId;
this.running = new AtomicBoolean();
- this.startTime = new Date();
+ this.startTime = Instant.now();
}
public int getTaskId() {
@@ -527,7 +527,7 @@
return State.SLEEPING;
}
- public Date getStartTime() {
+ public Instant getStartTime() {
return startTime;
}
diff --git a/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java b/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
index 27d5da9..befdb58 100644
--- a/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
+++ b/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
@@ -135,7 +135,7 @@
private PersonIdent createPersonIdent(IdentifiedUser user) {
PersonIdent serverIdent = serverIdentProvider.get();
- return user.newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone());
+ return user.newCommitterIdent(serverIdent);
}
}
@@ -215,11 +215,7 @@
public void setAuthor(IdentifiedUser author) {
this.author = author;
- getCommitBuilder()
- .setAuthor(
- author.newCommitterIdent(
- getCommitBuilder().getCommitter().getWhen(),
- getCommitBuilder().getCommitter().getTimeZone()));
+ getCommitBuilder().setAuthor(author.newCommitterIdent(getCommitBuilder().getCommitter()));
}
public void setAllowEmpty(boolean allowEmpty) {
diff --git a/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java b/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java
index 72483af..12666f9 100644
--- a/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java
+++ b/java/com/google/gerrit/server/git/receive/HackPushNegotiateHook.java
@@ -80,13 +80,13 @@
r =
rp.getRepository().getRefDatabase().getRefs().stream()
.collect(toMap(Ref::getName, x -> x));
+ rp.setAdvertisedRefs(r, history(r.values(), rp));
} catch (ServiceMayNotContinueException e) {
throw e;
} catch (IOException e) {
throw new ServiceMayNotContinueException(e);
}
}
- rp.setAdvertisedRefs(r, history(r.values(), rp));
}
private Set<ObjectId> history(Collection<Ref> refs, ReceivePack rp) {
diff --git a/java/com/google/gerrit/server/git/receive/LazyPostReceiveHookChain.java b/java/com/google/gerrit/server/git/receive/LazyPostReceiveHookChain.java
index a19dbac..a562659 100644
--- a/java/com/google/gerrit/server/git/receive/LazyPostReceiveHookChain.java
+++ b/java/com/google/gerrit/server/git/receive/LazyPostReceiveHookChain.java
@@ -72,7 +72,7 @@
String.format(
"%s request failed for project %s with [%s]",
REPOSITORY_SIZE_GROUP, project, a.errorMessage());
- logger.atWarning().log(msg);
+ logger.atWarning().log("%s", msg);
throw new RuntimeException(msg);
}
}
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 55d7af0..4e22933 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -747,14 +747,19 @@
return;
}
- if (!magicCommands.isEmpty()) {
- metrics.pushCount.increment("magic", project.getName(), getUpdateType(magicCommands));
- }
- if (!regularCommands.isEmpty()) {
- metrics.pushCount.increment("direct", project.getName(), getUpdateType(regularCommands));
- }
-
try {
+ if (!magicCommands.isEmpty()) {
+ parseMagicBranch(Iterables.getLast(magicCommands));
+ // Using the submit option submits the created change(s) immediately without checking labels
+ // nor submit rules. Hence we shouldn't record such pushes as "magic" which implies that
+ // code review is being done.
+ String pushKind = magicBranch != null && magicBranch.submit ? "direct_submit" : "magic";
+ metrics.pushCount.increment(pushKind, project.getName(), getUpdateType(magicCommands));
+ }
+ if (!regularCommands.isEmpty()) {
+ metrics.pushCount.increment("direct", project.getName(), getUpdateType(regularCommands));
+ }
+
if (!regularCommands.isEmpty()) {
handleRegularCommands(regularCommands, progress);
return;
@@ -763,7 +768,6 @@
boolean first = true;
for (ReceiveCommand cmd : magicCommands) {
if (first) {
- parseMagicBranch(cmd);
first = false;
} else {
reject(cmd, "duplicate request");
@@ -777,7 +781,7 @@
Task newProgress = progress.beginSubTask("new", UNKNOWN);
Task replaceProgress = progress.beginSubTask("updated", UNKNOWN);
- List<CreateRequest> newChanges = Collections.emptyList();
+ ImmutableList<CreateRequest> newChanges = ImmutableList.of();
try {
if (magicBranch != null && magicBranch.cmd.getResult() == NOT_ATTEMPTED) {
try {
@@ -836,7 +840,7 @@
Map<BranchNameKey, ReceiveCommand> branches;
try (BatchUpdate bu =
batchUpdateFactory.create(
- project.getNameKey(), user.materializedCopy(), TimeUtil.nowTs());
+ project.getNameKey(), user.materializedCopy(), TimeUtil.now());
ObjectInserter ins = repo.newObjectInserter();
ObjectReader reader = ins.newReader();
RevWalk rw = new RevWalk(reader);
@@ -858,7 +862,7 @@
submissionExecutor.execute(ImmutableList.of(bu));
- orm.setContext(TimeUtil.nowTs(), user, NotifyResolver.Result.none());
+ orm.setContext(TimeUtil.now(), user, NotifyResolver.Result.none());
submissionExecutor.afterExecutions(orm);
branches = bu.getSuccessfullyUpdatedBranches(false);
@@ -1005,7 +1009,8 @@
addMessage(changeFormatter.changeUpdated(input));
}
- private void insertChangesAndPatchSets(List<CreateRequest> newChanges, Task replaceProgress) {
+ private void insertChangesAndPatchSets(
+ ImmutableList<CreateRequest> newChanges, Task replaceProgress) {
try (TraceTimer traceTimer =
newTimer(
"insertChangesAndPatchSets", Metadata.builder().resourceCount(newChanges.size()))) {
@@ -1021,7 +1026,7 @@
try (BatchUpdate bu =
batchUpdateFactory.create(
- project.getNameKey(), user.materializedCopy(), TimeUtil.nowTs());
+ project.getNameKey(), user.materializedCopy(), TimeUtil.now());
ObjectInserter ins = repo.newObjectInserter();
ObjectReader reader = ins.newReader();
RevWalk rw = new RevWalk(reader)) {
@@ -1426,6 +1431,12 @@
private void parseCreate(ReceiveCommand cmd)
throws PermissionBackendException, NoSuchProjectException, IOException {
try (TraceTimer traceTimer = newTimer("parseCreate")) {
+ if (repo.resolve(cmd.getRefName()) != null) {
+ reject(
+ cmd,
+ String.format("Cannot create ref '%s' because it already exists.", cmd.getRefName()));
+ return;
+ }
RevObject obj;
try {
obj = receivePack.getRevWalk().parseAny(cmd.getNewId());
@@ -2228,7 +2239,8 @@
comment.message.length()))
.collect(toImmutableList());
CommentValidationContext ctx =
- CommentValidationContext.create(change.getChangeId(), change.getProject().get());
+ CommentValidationContext.create(
+ change.getChangeId(), change.getProject().get(), change.getDest().branch());
ImmutableList<CommentValidationFailure> commentValidationFailures =
PublishCommentUtil.findInvalidComments(ctx, commentValidators, draftsForValidation);
magicBranch.setWithholdComments(!commentValidationFailures.isEmpty());
@@ -2244,7 +2256,7 @@
}
}
- private void warnAboutMissingChangeId(List<CreateRequest> newChanges) {
+ private void warnAboutMissingChangeId(ImmutableList<CreateRequest> newChanges) {
for (CreateRequest create : newChanges) {
try {
receivePack.getRevWalk().parseBody(create.commit);
@@ -2261,7 +2273,7 @@
}
}
- private List<CreateRequest> selectNewAndReplacedChangesFromMagicBranch(Task newProgress)
+ private ImmutableList<CreateRequest> selectNewAndReplacedChangesFromMagicBranch(Task newProgress)
throws IOException {
try (TraceTimer traceTimer = newTimer("selectNewAndReplacedChangesFromMagicBranch")) {
logger.atFine().log("Finding new and replaced changes");
@@ -2276,7 +2288,7 @@
try {
RevCommit start = setUpWalkForSelectingChanges();
if (start == null) {
- return Collections.emptyList();
+ return ImmutableList.of();
}
LinkedHashMap<RevCommit, ChangeLookup> pending = new LinkedHashMap<>();
@@ -2354,7 +2366,7 @@
reject(
magicBranch.cmd,
"the number of pushed changes in a batch exceeds the max limit " + maxBatchChanges);
- return Collections.emptyList();
+ return ImmutableList.of();
}
if (commitAlreadyTracked) {
@@ -2387,7 +2399,7 @@
if (!validationResult.isValid()) {
// Not a change the user can propose? Abort as early as possible.
logger.atFine().log("Aborting early due to invalid commit");
- return Collections.emptyList();
+ return ImmutableList.of();
}
// Don't allow merges to be uploaded in commit chain via all-not-in-target
@@ -2424,7 +2436,7 @@
if (newChangeIds.contains(p.changeKey)) {
logger.atFine().log("Multiple commits with Change-Id %s", p.changeKey);
reject(magicBranch.cmd, SAME_CHANGE_ID_IN_MULTIPLE_CHANGES);
- return Collections.emptyList();
+ return ImmutableList.of();
}
List<ChangeData> changes = p.destChanges;
@@ -2440,7 +2452,7 @@
// this error message as Change-Id should be unique per branch.
//
reject(magicBranch.cmd, p.changeKey.get() + " has duplicates");
- return Collections.emptyList();
+ return ImmutableList.of();
}
if (changes.size() == 1) {
@@ -2465,13 +2477,13 @@
magicBranch.cmd, false, changes.get(0).change(), p.commit)) {
continue;
}
- return Collections.emptyList();
+ return ImmutableList.of();
}
if (changes.isEmpty()) {
if (!isValidChangeId(p.changeKey.get())) {
reject(magicBranch.cmd, "invalid Change-Id");
- return Collections.emptyList();
+ return ImmutableList.of();
}
// In case the change look up from the index failed,
@@ -2479,7 +2491,7 @@
if (foundInExistingPatchSets(receivePackRefCache.patchSetIdsFromObjectId(p.commit))) {
if (pending.size() == 1) {
reject(magicBranch.cmd, "commit(s) already exists (as current patchset)");
- return Collections.emptyList();
+ return ImmutableList.of();
}
itr.remove();
continue;
@@ -2499,11 +2511,11 @@
if (newChanges.isEmpty() && replaceByChange.isEmpty()) {
reject(magicBranch.cmd, "no new changes");
- return Collections.emptyList();
+ return ImmutableList.of();
}
if (!newChanges.isEmpty() && magicBranch.edit) {
reject(magicBranch.cmd, "edit is not supported for new changes");
- return newChanges;
+ return ImmutableList.copyOf(newChanges);
}
SortedSetMultimap<ObjectId, String> groups = groupCollector.getGroups();
@@ -2517,10 +2529,10 @@
replace.groups = ImmutableList.copyOf(groups.get(replace.newCommitId));
}
for (UpdateGroupsRequest update : updateGroups) {
- update.groups = ImmutableList.copyOf((groups.get(update.commit)));
+ update.groups = ImmutableList.copyOf(groups.get(update.commit));
}
logger.atFine().log("Finished updating groups from GroupCollector");
- return newChanges;
+ return ImmutableList.copyOf(newChanges);
}
}
@@ -2823,7 +2835,7 @@
}
}
- private void preparePatchSetsForReplace(List<CreateRequest> newChanges) {
+ private void preparePatchSetsForReplace(ImmutableList<CreateRequest> newChanges) {
try (TraceTimer traceTimer =
newTimer(
"preparePatchSetsForReplace", Metadata.builder().resourceCount(newChanges.size()))) {
@@ -3450,7 +3462,7 @@
"autoCloseChanges",
updateFactory -> {
try (BatchUpdate bu =
- updateFactory.create(projectState.getNameKey(), user, TimeUtil.nowTs());
+ updateFactory.create(projectState.getNameKey(), user, TimeUtil.now());
ObjectInserter ins = repo.newObjectInserter();
ObjectReader reader = ins.newReader();
RevWalk rw = new RevWalk(reader)) {
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
index cc203ad..e545c70 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
@@ -91,7 +91,11 @@
.filter(ReceiveCommitsAdvertiseRefsHook::skip)
.collect(toImmutableList())
.forEach(r -> advertisedRefs.remove(r));
- rp.setAdvertisedRefs(advertisedRefs, advertiseOpenChanges(rp.getRepository()));
+ try {
+ rp.setAdvertisedRefs(advertisedRefs, advertiseOpenChanges(rp.getRepository()));
+ } catch (IOException e) {
+ throw new ServiceMayNotContinueException(e);
+ }
}
private Set<ObjectId> advertiseOpenChanges(Repository repo)
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveConstants.java b/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
index df1888b..e50482d 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
@@ -21,7 +21,7 @@
@VisibleForTesting
public static final String ONLY_USERS_WITH_TOGGLE_WIP_STATE_PERM_CAN_MODIFY_WIP =
- "only users with Toogle-Wip-State permission can modify Work-in-Progress";
+ "only users with Toggle-Wip-State permission can modify Work-in-Progress";
static final String COMMAND_REJECTION_MESSAGE_FOOTER =
"Contact an administrator to fix the permissions";
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index a9ef70e..197183f 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -48,13 +48,13 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.approval.ApprovalsUtil;
-import com.google.gerrit.server.change.AddReviewersOp;
import com.google.gerrit.server.change.ChangeKindCache;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.ReviewerModifier;
import com.google.gerrit.server.change.ReviewerModifier.InternalReviewerInput;
import com.google.gerrit.server.change.ReviewerModifier.ReviewerModification;
import com.google.gerrit.server.change.ReviewerModifier.ReviewerModificationList;
+import com.google.gerrit.server.change.ReviewerOp;
import com.google.gerrit.server.config.SendEmailExecutor;
import com.google.gerrit.server.config.UrlFormatter;
import com.google.gerrit.server.extensions.events.CommentAdded;
@@ -345,9 +345,8 @@
update.putReviewer(ctx.getAccountId(), REVIEWER);
}
- // Approvals that are being set in the new patch-set during this operation are not available yet
- // outside of the scope of this method. Only copied approvals are set here.
- approvalsUtil.byPatchSet(ctx.getNotes(), newPatchSet).forEach(a -> update.putCopiedApproval(a));
+ approvalsUtil.persistCopiedApprovals(
+ ctx.getNotes(), newPatchSet, ctx.getRevWalk(), ctx.getRepoView().getConfig(), update);
mailMessage = insertChangeMessage(update, ctx, reviewMessage);
if (mergedByPushOp == null) {
@@ -407,8 +406,7 @@
return input;
}
- private String insertChangeMessage(ChangeUpdate update, ChangeContext ctx, String reviewMessage)
- throws IOException {
+ private String insertChangeMessage(ChangeUpdate update, ChangeContext ctx, String reviewMessage) {
String approvalMessage =
ApprovalsUtil.renderMessageWithApprovals(
patchSetId.get(), approvals, scanLabels(ctx, approvals));
@@ -452,18 +450,13 @@
}
}
- private Map<String, PatchSetApproval> scanLabels(ChangeContext ctx, Map<String, Short> approvals)
- throws IOException {
+ private Map<String, PatchSetApproval> scanLabels(
+ ChangeContext ctx, Map<String, Short> approvals) {
Map<String, PatchSetApproval> current = new HashMap<>();
// We optimize here and only retrieve current when approvals provided
if (!approvals.isEmpty()) {
for (PatchSetApproval a :
- approvalsUtil.byPatchSetUser(
- ctx.getNotes(),
- priorPatchSetId,
- ctx.getAccountId(),
- ctx.getRevWalk(),
- ctx.getRepoView().getConfig())) {
+ approvalsUtil.byPatchSetUser(ctx.getNotes(), priorPatchSetId, ctx.getAccountId())) {
if (a.isLegacySubmit()) {
continue;
}
@@ -538,13 +531,13 @@
emailSender.addReviewers(
Streams.concat(
oldRecipients.getReviewers().stream(),
- reviewerAdditions.flattenResults(AddReviewersOp.Result::addedReviewers).stream()
+ reviewerAdditions.flattenResults(ReviewerOp.Result::addedReviewers).stream()
.map(PatchSetApproval::accountId))
.collect(toImmutableSet()));
emailSender.addExtraCC(
Streams.concat(
oldRecipients.getCcOnly().stream(),
- reviewerAdditions.flattenResults(AddReviewersOp.Result::addedCCs).stream())
+ reviewerAdditions.flattenResults(ReviewerOp.Result::addedCCs).stream())
.collect(toImmutableSet()));
emailSender.setMessageId(
messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSetId));
diff --git a/java/com/google/gerrit/server/git/receive/testing/TestRefAdvertiser.java b/java/com/google/gerrit/server/git/receive/testing/TestRefAdvertiser.java
index c54ab25..4d2805d 100644
--- a/java/com/google/gerrit/server/git/receive/testing/TestRefAdvertiser.java
+++ b/java/com/google/gerrit/server/git/receive/testing/TestRefAdvertiser.java
@@ -19,6 +19,8 @@
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
@@ -37,12 +39,13 @@
@VisibleForTesting
@AutoValue
public abstract static class Result {
- public abstract Map<String, Ref> allRefs();
+ public abstract ImmutableMap<String, Ref> allRefs();
- public abstract Set<ObjectId> additionalHaves();
+ public abstract ImmutableSet<ObjectId> additionalHaves();
public static Result create(Map<String, Ref> allRefs, Set<ObjectId> additionalHaves) {
- return new AutoValue_TestRefAdvertiser_Result(allRefs, additionalHaves);
+ return new AutoValue_TestRefAdvertiser_Result(
+ ImmutableMap.copyOf(allRefs), ImmutableSet.copyOf(additionalHaves));
}
}
diff --git a/java/com/google/gerrit/server/git/validators/CommentCumulativeSizeValidator.java b/java/com/google/gerrit/server/git/validators/CommentCumulativeSizeValidator.java
index 6e640f3..b887323 100644
--- a/java/com/google/gerrit/server/git/validators/CommentCumulativeSizeValidator.java
+++ b/java/com/google/gerrit/server/git/validators/CommentCumulativeSizeValidator.java
@@ -23,7 +23,6 @@
import com.google.gerrit.extensions.validators.CommentValidationContext;
import com.google.gerrit.extensions.validators.CommentValidationFailure;
import com.google.gerrit.extensions.validators.CommentValidator;
-import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.inject.Inject;
@@ -32,7 +31,8 @@
/**
* Limits the total size of all comments and change messages to prevent space/time complexity
- * issues. Note that autogenerated change messages are not subject to validation.
+ * issues. Note that autogenerated change messages are not subject to validation. However, we still
+ * count autogenerated messages for the limit (which will be notified on a further comment).
*/
public class CommentCumulativeSizeValidator implements CommentValidator {
public static final int DEFAULT_CUMULATIVE_COMMENT_SIZE_LIMIT = 3 << 20;
@@ -60,17 +60,11 @@
notes.getRobotComments().values().stream())
.mapToInt(Comment::getApproximateSize)
.sum()
- + notes.getChangeMessages().stream()
- // Auto-generated change messages are not counted for the limit. This method is not
- // called when those change messages are created, but we should also skip them when
- // counting the size for unrelated messages.
- .filter(cm -> !ChangeMessagesUtil.isAutogenerated(cm.getTag()))
- .mapToInt(cm -> cm.getMessage().length())
- .sum();
+ + notes.getChangeMessages().stream().mapToInt(cm -> cm.getMessage().length()).sum();
int newCumulativeSize =
comments.stream().mapToInt(CommentForValidation::getApproximateSize).sum();
ImmutableList.Builder<CommentValidationFailure> failures = ImmutableList.builder();
- if (!comments.isEmpty() && existingCumulativeSize + newCumulativeSize > maxCumulativeSize) {
+ if (!comments.isEmpty() && !isEnoughSpace(notes, newCumulativeSize, maxCumulativeSize)) {
// This warning really applies to the set of all comments, but we need to pick one to attach
// the message to.
CommentForValidation commentForFailureMessage = Iterables.getLast(comments);
@@ -84,4 +78,19 @@
}
return failures.build();
}
+
+ /**
+ * Returns {@code true} if there is available space and the new size that we wish to add is less
+ * than the maximum allowed size. {@code false} otherwise (if there is not enough space).
+ */
+ public static boolean isEnoughSpace(ChangeNotes notes, int addedBytes, int maxCumulativeSize) {
+ int existingCumulativeSize =
+ Stream.concat(
+ notes.getHumanComments().values().stream(),
+ notes.getRobotComments().values().stream())
+ .mapToInt(Comment::getApproximateSize)
+ .sum()
+ + notes.getChangeMessages().stream().mapToInt(cm -> cm.getMessage().length()).sum();
+ return existingCumulativeSize + addedBytes < maxCumulativeSize;
+ }
}
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 056407e..8049df4 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -441,7 +441,7 @@
// This happens e.g. for cherrypicks.
if (!receiveEvent.command.getRefName().startsWith(REFS_CHANGES)) {
logger.atWarning().withCause(e).log(
- "Failed to validate file count for commit: %s", receiveEvent.commit.toString());
+ "Failed to validate file count for commit: %s", receiveEvent.commit);
}
}
return Collections.emptyList();
@@ -778,9 +778,7 @@
}
return Collections.emptyList();
} catch (IOException e) {
- String m = "error checking banned commits";
- logger.atWarning().withCause(e).log(m);
- throw new CommitValidationException(m, e);
+ throw new CommitValidationException("error checking banned commits", e);
}
}
}
@@ -819,9 +817,7 @@
}
return msgs;
} catch (IOException | ConfigInvalidException e) {
- String m = "error validating external IDs";
- logger.atWarning().withCause(e).log(m);
- throw new CommitValidationException(m, e);
+ throw new CommitValidationException("error validating external IDs", e);
}
}
return Collections.emptyList();
@@ -876,9 +872,8 @@
.collect(toList()));
}
} catch (IOException e) {
- String m = String.format("Validating update for account %s failed", accountId.get());
- logger.atSevere().withCause(e).log(m);
- throw new CommitValidationException(m, e);
+ throw new CommitValidationException(
+ String.format("Validating update for account %s failed", accountId.get()), e);
}
return Collections.emptyList();
}
@@ -969,6 +964,9 @@
try {
return new URL(canonicalWebUrl).getHost();
} catch (MalformedURLException ignored) {
+ logger.atWarning().log(
+ "configured canonical web URL is invalid, using system default: %s",
+ ignored.getMessage());
}
}
diff --git a/java/com/google/gerrit/server/git/validators/MergeValidators.java b/java/com/google/gerrit/server/git/validators/MergeValidators.java
index 6b145ca..c514969 100644
--- a/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -235,7 +235,7 @@
String oldValue =
destProject.getPluginConfig(e.getPluginName()).getString(e.getExportName());
- if ((!Objects.equals(value, oldValue)) && !configEntry.isEditable(destProject)) {
+ if (!Objects.equals(value, oldValue) && !configEntry.isEditable(destProject)) {
throw new MergeValidationException(PLUGIN_VALUE_NOT_EDITABLE);
}
diff --git a/java/com/google/gerrit/server/git/validators/RefOperationValidators.java b/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
index f3b6983..ddf5972 100644
--- a/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
+++ b/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
@@ -108,7 +108,7 @@
String.format(
"Ref \"%s\" %S in project %s validation failed",
event.command.getRefName(), event.command.getType(), event.project.getName());
- logger.atSevere().log(header);
+ logger.atSevere().log("%s", header);
throw new RefOperationValidationException(
header, messages.stream().filter(ValidationMessage::isError).collect(toImmutableList()));
}
diff --git a/java/com/google/gerrit/server/group/GroupAuditService.java b/java/com/google/gerrit/server/group/GroupAuditService.java
index 30e5d3c..21959ec 100644
--- a/java/com/google/gerrit/server/group/GroupAuditService.java
+++ b/java/com/google/gerrit/server/group/GroupAuditService.java
@@ -18,7 +18,7 @@
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.server.AuditEvent;
-import java.sql.Timestamp;
+import java.time.Instant;
public interface GroupAuditService {
void dispatch(AuditEvent action);
@@ -27,23 +27,23 @@
Account.Id actor,
AccountGroup.UUID updatedGroup,
ImmutableSet<Account.Id> addedMembers,
- Timestamp addedOn);
+ Instant addedOn);
void dispatchDeleteMembers(
Account.Id actor,
AccountGroup.UUID updatedGroup,
ImmutableSet<Account.Id> deletedMembers,
- Timestamp deletedOn);
+ Instant deletedOn);
void dispatchAddSubgroups(
Account.Id actor,
AccountGroup.UUID updatedGroup,
ImmutableSet<AccountGroup.UUID> addedSubgroups,
- Timestamp addedOn);
+ Instant addedOn);
void dispatchDeleteSubgroups(
Account.Id actor,
AccountGroup.UUID updatedGroup,
ImmutableSet<AccountGroup.UUID> deletedSubgroups,
- Timestamp deletedOn);
+ Instant deletedOn);
}
diff --git a/java/com/google/gerrit/server/group/GroupResource.java b/java/com/google/gerrit/server/group/GroupResource.java
index b0e81ec..dfcdbd7 100644
--- a/java/com/google/gerrit/server/group/GroupResource.java
+++ b/java/com/google/gerrit/server/group/GroupResource.java
@@ -22,8 +22,7 @@
import java.util.Optional;
public class GroupResource implements RestResource {
- public static final TypeLiteral<RestView<GroupResource>> GROUP_KIND =
- new TypeLiteral<RestView<GroupResource>>() {};
+ public static final TypeLiteral<RestView<GroupResource>> GROUP_KIND = new TypeLiteral<>() {};
private final GroupControl control;
diff --git a/java/com/google/gerrit/server/group/InternalGroupDescription.java b/java/com/google/gerrit/server/group/InternalGroupDescription.java
index 62ebcfe..984daea 100644
--- a/java/com/google/gerrit/server/group/InternalGroupDescription.java
+++ b/java/com/google/gerrit/server/group/InternalGroupDescription.java
@@ -23,7 +23,7 @@
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.GroupDescription;
import com.google.gerrit.entities.InternalGroup;
-import java.sql.Timestamp;
+import java.time.Instant;
public class InternalGroupDescription implements GroupDescription.Internal {
@@ -77,7 +77,7 @@
}
@Override
- public Timestamp getCreatedOn() {
+ public Instant getCreatedOn() {
return internalGroup.getCreatedOn();
}
diff --git a/java/com/google/gerrit/server/group/MemberResource.java b/java/com/google/gerrit/server/group/MemberResource.java
index b12cadd..de8cc02 100644
--- a/java/com/google/gerrit/server/group/MemberResource.java
+++ b/java/com/google/gerrit/server/group/MemberResource.java
@@ -19,8 +19,7 @@
import com.google.inject.TypeLiteral;
public class MemberResource extends GroupResource {
- public static final TypeLiteral<RestView<MemberResource>> MEMBER_KIND =
- new TypeLiteral<RestView<MemberResource>>() {};
+ public static final TypeLiteral<RestView<MemberResource>> MEMBER_KIND = new TypeLiteral<>() {};
private final IdentifiedUser user;
diff --git a/java/com/google/gerrit/server/group/SubgroupResource.java b/java/com/google/gerrit/server/group/SubgroupResource.java
index 21356be..7d917a7 100644
--- a/java/com/google/gerrit/server/group/SubgroupResource.java
+++ b/java/com/google/gerrit/server/group/SubgroupResource.java
@@ -21,7 +21,7 @@
public class SubgroupResource extends GroupResource {
public static final TypeLiteral<RestView<SubgroupResource>> SUBGROUP_KIND =
- new TypeLiteral<RestView<SubgroupResource>>() {};
+ new TypeLiteral<>() {};
private final GroupDescription.Basic member;
diff --git a/java/com/google/gerrit/server/group/SystemGroupBackend.java b/java/com/google/gerrit/server/group/SystemGroupBackend.java
index 5d50d22..5a9b9e5 100644
--- a/java/com/google/gerrit/server/group/SystemGroupBackend.java
+++ b/java/com/google/gerrit/server/group/SystemGroupBackend.java
@@ -45,9 +45,9 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.NavigableMap;
import java.util.Optional;
import java.util.Set;
-import java.util.SortedMap;
import java.util.TreeMap;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
@@ -89,7 +89,7 @@
}
private final ImmutableSet<String> reservedNames;
- private final SortedMap<String, GroupReference> namesToGroups;
+ private final NavigableMap<String, GroupReference> namesToGroups;
private final ImmutableSet<String> names;
private final ImmutableMap<AccountGroup.UUID, GroupReference> uuids;
private final ImmutableSet<AccountGroup.UUID> externalUserMemberships;
@@ -97,7 +97,7 @@
@Inject
@VisibleForTesting
public SystemGroupBackend(@GerritServerConfig Config cfg) {
- SortedMap<String, GroupReference> n = new TreeMap<>();
+ NavigableMap<String, GroupReference> n = new TreeMap<>();
ImmutableMap.Builder<AccountGroup.UUID, GroupReference> u = ImmutableMap.builder();
ImmutableSet.Builder<String> reservedNamesBuilder = ImmutableSet.builder();
@@ -112,7 +112,7 @@
u.put(ref.getUUID(), ref);
}
reservedNames = reservedNamesBuilder.build();
- namesToGroups = Collections.unmodifiableSortedMap(n);
+ namesToGroups = Collections.unmodifiableNavigableMap(n);
names =
ImmutableSet.copyOf(
namesToGroups.values().stream().map(GroupReference::getName).collect(toSet()));
@@ -172,9 +172,10 @@
@Override
public Collection<GroupReference> suggest(String name, ProjectState project) {
String nameLC = name.toLowerCase(Locale.US);
- SortedMap<String, GroupReference> matches = namesToGroups.tailMap(nameLC);
+ NavigableMap<String, GroupReference> matches =
+ namesToGroups.tailMap(nameLC, /* inclusive= */ true);
if (matches.isEmpty()) {
- return Collections.emptyList();
+ return new ArrayList<>();
}
List<GroupReference> r = new ArrayList<>(matches.size());
diff --git a/java/com/google/gerrit/server/group/db/AuditLogReader.java b/java/com/google/gerrit/server/group/db/AuditLogReader.java
index d8f0a0f..3f7ef2c 100644
--- a/java/com/google/gerrit/server/group/db/AuditLogReader.java
+++ b/java/com/google/gerrit/server/group/db/AuditLogReader.java
@@ -31,7 +31,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@@ -139,6 +139,9 @@
return result.stream().map(AccountGroupByIdAudit.Builder::build).collect(toImmutableList());
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
private Optional<ParsedCommit> parse(AccountGroup.UUID uuid, RevCommit c) {
Optional<Account.Id> authorId = NoteDbUtil.parseIdent(c.getAuthorIdent());
if (!authorId.isPresent()) {
@@ -166,7 +169,7 @@
return Optional.of(
new AutoValue_AuditLogReader_ParsedCommit(
authorId.get(),
- new Timestamp(c.getAuthorIdent().getWhen().getTime()),
+ c.getAuthorIdent().getWhen().toInstant(),
ImmutableList.copyOf(addedMembers),
ImmutableList.copyOf(removedMembers),
ImmutableList.copyOf(addedSubgroups),
@@ -257,7 +260,7 @@
abstract static class ParsedCommit {
abstract Account.Id authorId();
- abstract Timestamp when();
+ abstract Instant when();
abstract ImmutableList<Account.Id> addedMembers();
diff --git a/java/com/google/gerrit/server/group/db/GroupConfig.java b/java/com/google/gerrit/server/group/db/GroupConfig.java
index c187186..71cc08c 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfig.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfig.java
@@ -35,8 +35,9 @@
import com.google.gerrit.server.git.meta.VersionedMetaData;
import com.google.gerrit.server.util.time.TimeUtil;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Arrays;
+import java.util.Date;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Pattern;
@@ -279,7 +280,7 @@
rw.markStart(revision);
rw.sort(RevSort.REVERSE);
RevCommit earliestCommit = rw.next();
- Timestamp createdOn = new Timestamp(earliestCommit.getCommitTime() * 1000L);
+ Instant createdOn = Instant.ofEpochSecond(earliestCommit.getCommitTime());
Config config = readConfig(GROUP_CONFIG_FILE);
ImmutableSet<Account.Id> members = readMembers();
@@ -299,6 +300,9 @@
return c;
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Override
protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
checkLoaded();
@@ -314,11 +318,11 @@
// Commit timestamps are internally truncated to seconds. To return the correct 'createdOn' time
// for new groups, we explicitly need to truncate the timestamp here.
- Timestamp commitTimestamp =
+ Instant commitTimestamp =
TimeUtil.truncateToSecond(
- groupDelta.flatMap(GroupDelta::getUpdatedOn).orElseGet(TimeUtil::nowTs));
- commit.setAuthor(new PersonIdent(commit.getAuthor(), commitTimestamp));
- commit.setCommitter(new PersonIdent(commit.getCommitter(), commitTimestamp));
+ groupDelta.flatMap(GroupDelta::getUpdatedOn).orElseGet(TimeUtil::now));
+ commit.setAuthor(new PersonIdent(commit.getAuthor(), Date.from(commitTimestamp)));
+ commit.setCommitter(new PersonIdent(commit.getCommitter(), Date.from(commitTimestamp)));
InternalGroup updatedGroup = updateGroup(commitTimestamp);
@@ -346,7 +350,7 @@
return Optional.empty();
}
- private InternalGroup updateGroup(Timestamp commitTimestamp)
+ private InternalGroup updateGroup(Instant commitTimestamp)
throws IOException, ConfigInvalidException {
Config config = updateGroupProperties();
@@ -358,7 +362,7 @@
loadedGroup.map(InternalGroup::getSubgroups).orElseGet(ImmutableSet::of);
Optional<ImmutableSet<AccountGroup.UUID>> updatedSubgroups = updateSubgroups(originalSubgroups);
- Timestamp createdOn = loadedGroup.map(InternalGroup::getCreatedOn).orElse(commitTimestamp);
+ Instant createdOn = loadedGroup.map(InternalGroup::getCreatedOn).orElse(commitTimestamp);
return createFrom(
groupUuid,
@@ -453,7 +457,7 @@
Config config,
ImmutableSet<Account.Id> members,
ImmutableSet<AccountGroup.UUID> subgroups,
- Timestamp createdOn,
+ Instant createdOn,
ObjectId refState)
throws ConfigInvalidException {
InternalGroup.Builder group = InternalGroup.builder();
diff --git a/java/com/google/gerrit/server/group/db/GroupDelta.java b/java/com/google/gerrit/server/group/db/GroupDelta.java
index 69cb936..ad9c8bd 100644
--- a/java/com/google/gerrit/server/group/db/GroupDelta.java
+++ b/java/com/google/gerrit/server/group/db/GroupDelta.java
@@ -18,7 +18,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Optional;
import java.util.Set;
@@ -107,7 +107,7 @@
* in the audit log. For this reason, specifying this field will have an effect on the resulting
* audit log.
*/
- public abstract Optional<Timestamp> getUpdatedOn();
+ public abstract Optional<Instant> getUpdatedOn();
public abstract Builder toBuilder();
@@ -184,12 +184,12 @@
public abstract SubgroupModification getSubgroupModification();
/**
- * Defines the {@code Timestamp} to be used for the NoteDb commits of the update. If not
- * specified, the current {@code Timestamp} when creating the commit will be used.
+ * Defines the {@code Instant} to be used for the NoteDb commits of the update. If not
+ * specified, the current {@code Instant} when creating the commit will be used.
*
* <p>See {@link #getUpdatedOn()}
*/
- public abstract Builder setUpdatedOn(Timestamp timestamp);
+ public abstract Builder setUpdatedOn(Instant timestamp);
public abstract GroupDelta build();
}
diff --git a/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java b/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
index 24bcaf0..c648d11 100644
--- a/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
+++ b/java/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyChecker.java
@@ -231,7 +231,7 @@
public static void ensureConsistentWithGroupNameNotes(
Repository allUsersRepo, InternalGroup group) throws IOException {
- List<ConsistencyCheckInfo.ConsistencyProblemInfo> problems =
+ ImmutableList<ConsistencyCheckInfo.ConsistencyProblemInfo> problems =
GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
allUsersRepo, group.getNameKey(), group.getGroupUUID());
problems.forEach(GroupsNoteDbConsistencyChecker::logConsistencyProblem);
@@ -246,7 +246,7 @@
* @return a list of {@code ConsistencyProblemInfo} containing the problem details.
*/
@VisibleForTesting
- static List<ConsistencyProblemInfo> checkWithGroupNameNotes(
+ static ImmutableList<ConsistencyProblemInfo> checkWithGroupNameNotes(
Repository allUsersRepo, AccountGroup.NameKey groupName, AccountGroup.UUID groupUUID)
throws IOException {
try {
@@ -273,7 +273,7 @@
problems.add(
warning("group note of name '%s' claims to represent name of '%s'", name, actualName));
}
- return problems;
+ return ImmutableList.copyOf(problems);
} catch (ConfigInvalidException e) {
return ImmutableList.of(
warning("fail to check consistency with group name notes: %s", e.getMessage()));
@@ -287,9 +287,9 @@
public static void logConsistencyProblem(ConsistencyProblemInfo p) {
if (p.status == ConsistencyProblemInfo.Status.WARNING) {
- logger.atWarning().log(p.message);
+ logger.atWarning().log("%s", p.message);
} else {
- logger.atSevere().log(p.message);
+ logger.atSevere().log("%s", p.message);
}
}
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
index 9aa5cfd..c0c934b 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -50,7 +50,7 @@
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -248,7 +248,7 @@
}
private static PersonIdent createPersonIdent(PersonIdent ident, IdentifiedUser user) {
- return user.newCommitterIdent(ident.getWhen(), ident.getTimeZone());
+ return user.newCommitterIdent(ident);
}
/**
@@ -292,9 +292,9 @@
try (TraceTimer ignored =
TraceContext.newTimer(
"Updating group", Metadata.builder().groupUuid(groupUuid.get()).build())) {
- Optional<Timestamp> updatedOn = groupDelta.getUpdatedOn();
+ Optional<Instant> updatedOn = groupDelta.getUpdatedOn();
if (!updatedOn.isPresent()) {
- updatedOn = Optional.of(TimeUtil.nowTs());
+ updatedOn = Optional.of(TimeUtil.now());
groupDelta = groupDelta.toBuilder().setUpdatedOn(updatedOn.get()).build();
}
@@ -505,7 +505,7 @@
}
}
- private void dispatchAuditEventsOnGroupUpdate(UpdateResult result, Timestamp updatedOn) {
+ private void dispatchAuditEventsOnGroupUpdate(UpdateResult result, Instant updatedOn) {
if (!currentUser.isPresent()) {
return;
}
diff --git a/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java b/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
index 8a1221e..f422f6a 100644
--- a/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
+++ b/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
@@ -24,7 +24,7 @@
import com.google.common.truth.Subject;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.InternalGroup;
-import java.sql.Timestamp;
+import java.time.Instant;
import org.eclipse.jgit.lib.ObjectId;
public class InternalGroupSubject extends Subject {
@@ -79,7 +79,7 @@
return check("isVisibleToAll()").that(group.isVisibleToAll());
}
- public ComparableSubject<Timestamp> createdOn() {
+ public ComparableSubject<Instant> createdOn() {
isNotNull();
return check("getCreatedOn()").that(group.getCreatedOn());
}
diff --git a/java/com/google/gerrit/server/index/IndexModule.java b/java/com/google/gerrit/server/index/IndexModule.java
index a729863..9ad7cdb 100644
--- a/java/com/google/gerrit/server/index/IndexModule.java
+++ b/java/com/google/gerrit/server/index/IndexModule.java
@@ -17,6 +17,7 @@
import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
+import com.google.common.base.Ticker;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
@@ -114,6 +115,9 @@
@Override
protected void configure() {
factory(MultiProgressMonitor.Factory.class);
+ OptionalBinder.newOptionalBinder(binder(), Ticker.class)
+ .setDefault()
+ .toInstance(Ticker.systemTicker());
bind(AccountIndexRewriter.class);
bind(AccountIndexCollection.class);
diff --git a/java/com/google/gerrit/server/index/account/AccountField.java b/java/com/google/gerrit/server/index/account/AccountField.java
index 0dd22ce..416b175 100644
--- a/java/com/google/gerrit/server/index/account/AccountField.java
+++ b/java/com/google/gerrit/server/index/account/AccountField.java
@@ -116,8 +116,9 @@
public static final FieldDef<AccountState, String> PREFERRED_EMAIL_EXACT =
exact("preferredemail_exact").build(a -> a.account().preferredEmail());
+ // TODO(issue-15518): Migrate type for timestamp index fields from Timestamp to Instant
public static final FieldDef<AccountState, Timestamp> REGISTERED =
- timestamp("registered").build(a -> a.account().registeredOn());
+ timestamp("registered").build(a -> Timestamp.from(a.account().registeredOn()));
public static final FieldDef<AccountState, String> USERNAME =
exact("username").build(a -> a.userName().map(String::toLowerCase).orElse(""));
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index b1ff504..9f14926 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -261,7 +261,7 @@
this.failed.update(1);
}
- logger.atWarning().withCause(e).log(error);
+ logger.atWarning().withCause(e).log("%s", error);
verboseWriter.println(error);
}
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 4a2419b..3d5dca8 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -33,6 +33,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
+import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@@ -55,6 +56,7 @@
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.entities.converter.ChangeProtoConverter;
import com.google.gerrit.entities.converter.PatchSetApprovalProtoConverter;
import com.google.gerrit.entities.converter.PatchSetProtoConverter;
@@ -150,19 +152,29 @@
public static final FieldDef<ChangeData, String> FUZZY_TOPIC =
fullText("topic5").build(ChangeField::getTopic);
+ /** Topic, a short annotation on the branch. */
+ public static final FieldDef<ChangeData, String> PREFIX_TOPIC =
+ prefix("topic6").build(ChangeField::getTopic);
+
/** Submission id assigned by MergeOp. */
public static final FieldDef<ChangeData, String> SUBMISSIONID =
exact(ChangeQueryBuilder.FIELD_SUBMISSIONID).build(changeGetter(Change::getSubmissionId));
/** Last update time since January 1, 1970. */
+ // TODO(issue-15518): Migrate type for timestamp index fields from Timestamp to Instant
public static final FieldDef<ChangeData, Timestamp> UPDATED =
- timestamp("updated2").stored().build(changeGetter(Change::getLastUpdatedOn));
+ timestamp("updated2")
+ .stored()
+ .build(changeGetter(change -> Timestamp.from(change.getLastUpdatedOn())));
/** 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)
.stored()
- .build(cd -> cd.getMergedOn().orElse(null), (cd, field) -> cd.setMergedOn(field));
+ .build(
+ cd -> cd.getMergedOn().map(Timestamp::from).orElse(null),
+ (cd, field) -> cd.setMergedOn(field != null ? field.toInstant() : null));
/** List of full file paths modified in the current patch set. */
public static final FieldDef<ChangeData, Iterable<String>> PATH =
@@ -193,6 +205,11 @@
fullText("hashtag2")
.buildRepeatable(cd -> cd.hashtags().stream().map(String::toLowerCase).collect(toSet()));
+ /** 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()));
+
/** Hashtags with original case. */
public static final FieldDef<ChangeData, Iterable<byte[]>> HASHTAG_CASE_AWARE =
storedOnly("_hashtag")
@@ -412,14 +429,31 @@
integer(ChangeQueryBuilder.FIELD_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)
+ .build(cd -> Boolean.TRUE.equals(cd.isPureRevert()) ? "1" : "0");
+
+ /**
+ * 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)
+ .build(
+ cd ->
+ // All submit requirements should be fulfilled
+ cd.submitRequirements().values().stream()
+ .allMatch(SubmitRequirementResult::fulfilled)
+ ? "1"
+ : "0");
+
@VisibleForTesting
static List<String> getReviewerFieldValues(ReviewerSet reviewers) {
List<String> r = new ArrayList<>(reviewers.asTable().size() * 2);
- for (Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> c :
- reviewers.asTable().cellSet()) {
+ for (Table.Cell<ReviewerStateInternal, Account.Id, Instant> c : reviewers.asTable().cellSet()) {
String v = getReviewerFieldValue(c.getRowKey(), c.getColumnKey());
r.add(v);
- r.add(v + ',' + c.getValue().getTime());
+ r.add(v + ',' + c.getValue().toEpochMilli());
}
return r;
}
@@ -431,7 +465,7 @@
@VisibleForTesting
static List<String> getReviewerByEmailFieldValues(ReviewerByEmailSet reviewersByEmail) {
List<String> r = new ArrayList<>(reviewersByEmail.asTable().size() * 2);
- for (Table.Cell<ReviewerStateInternal, Address, Timestamp> c :
+ for (Table.Cell<ReviewerStateInternal, Address, Instant> c :
reviewersByEmail.asTable().cellSet()) {
String v = getReviewerByEmailFieldValue(c.getRowKey(), c.getColumnKey());
r.add(v);
@@ -440,7 +474,7 @@
Address emailOnly = Address.create(c.getColumnKey().email());
r.add(getReviewerByEmailFieldValue(c.getRowKey(), emailOnly));
}
- r.add(v + ',' + c.getValue().getTime());
+ r.add(v + ',' + c.getValue().toEpochMilli());
}
return r;
}
@@ -450,8 +484,7 @@
}
public static ReviewerSet parseReviewerFieldValues(Change.Id changeId, Iterable<String> values) {
- ImmutableTable.Builder<ReviewerStateInternal, Account.Id, Timestamp> b =
- ImmutableTable.builder();
+ ImmutableTable.Builder<ReviewerStateInternal, Account.Id, Instant> b = ImmutableTable.builder();
for (String v : values) {
int i = v.indexOf(',');
@@ -493,7 +526,7 @@
"Failed to parse timestamp of reviewer field from change %s: %s", changeId.get(), v);
continue;
}
- Timestamp timestamp = new Timestamp(l);
+ Instant timestamp = Instant.ofEpochMilli(l);
b.put(reviewerState.get(), accountId.get(), timestamp);
}
@@ -502,7 +535,7 @@
public static ReviewerByEmailSet parseReviewerByEmailFieldValues(
Change.Id changeId, Iterable<String> values) {
- ImmutableTable.Builder<ReviewerStateInternal, Address, Timestamp> b = ImmutableTable.builder();
+ ImmutableTable.Builder<ReviewerStateInternal, Address, Instant> b = ImmutableTable.builder();
for (String v : values) {
int i = v.indexOf(',');
if (i < 0) {
@@ -546,7 +579,7 @@
changeId.get(), v);
continue;
}
- Timestamp timestamp = new Timestamp(l);
+ Instant timestamp = Instant.ofEpochMilli(l);
b.put(reviewerState.get(), address, timestamp);
}
@@ -612,47 +645,107 @@
private static Iterable<String> getLabels(ChangeData cd) {
Set<String> allApprovals = new HashSet<>();
Set<String> distinctApprovals = new HashSet<>();
+ Table<String, Short, Integer> voteCounts = HashBasedTable.create();
for (PatchSetApproval a : cd.currentApprovals()) {
if (a.value() != 0 && !a.isLegacySubmit()) {
- allApprovals.add(formatLabel(a.label(), a.value(), a.accountId()));
+ increment(voteCounts, a.label(), a.value());
Optional<LabelType> labelType = cd.getLabelTypes().byLabel(a.labelId());
- allApprovals.addAll(getMaxMinAnyLabels(a.label(), a.value(), labelType, a.accountId()));
- if (cd.change().getOwner().equals(a.accountId())) {
- allApprovals.add(formatLabel(a.label(), a.value(), ChangeQueryBuilder.OWNER_ACCOUNT_ID));
- allApprovals.addAll(
- getMaxMinAnyLabels(
- a.label(), a.value(), labelType, ChangeQueryBuilder.OWNER_ACCOUNT_ID));
- }
- if (!cd.currentPatchSet().uploader().equals(a.accountId())) {
- allApprovals.add(
- formatLabel(a.label(), a.value(), ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID));
- allApprovals.addAll(
- getMaxMinAnyLabels(
- a.label(), a.value(), labelType, ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID));
- }
+
+ allApprovals.add(formatLabel(a.label(), a.value(), a.accountId()));
+ allApprovals.addAll(getMagicLabelFormats(a.label(), a.value(), labelType, a.accountId()));
+ allApprovals.addAll(getLabelOwnerFormats(a, cd, labelType));
+ allApprovals.addAll(getLabelNonUploaderFormats(a, cd, labelType));
distinctApprovals.add(formatLabel(a.label(), a.value()));
- distinctApprovals.addAll(getMaxMinAnyLabels(a.label(), a.value(), labelType, null));
+ distinctApprovals.addAll(
+ getMagicLabelFormats(a.label(), a.value(), labelType, /* accountId= */ null));
}
}
allApprovals.addAll(distinctApprovals);
+ allApprovals.addAll(getCountLabelFormats(voteCounts, cd));
return allApprovals;
}
- private static List<String> getMaxMinAnyLabels(
+ private static void increment(Table<String, Short, Integer> table, String k1, short k2) {
+ if (!table.contains(k1, k2)) {
+ table.put(k1, k2, 1);
+ } else {
+ int val = table.get(k1, k2);
+ table.put(k1, k2, val + 1);
+ }
+ }
+
+ private static List<String> getCountLabelFormats(
+ Table<String, Short, Integer> voteCounts, ChangeData cd) {
+ List<String> allFormats = new ArrayList<>();
+ for (String label : voteCounts.rowMap().keySet()) {
+ Optional<LabelType> labelType = cd.getLabelTypes().byLabel(label);
+ Map<Short, Integer> row = voteCounts.row(label);
+ for (short vote : row.keySet()) {
+ int count = row.get(vote);
+ allFormats.addAll(getCountLabelFormats(labelType, label, vote, count));
+ }
+ }
+ return allFormats;
+ }
+
+ private static List<String> getCountLabelFormats(
+ Optional<LabelType> labelType, String label, short vote, int count) {
+ List<String> formats =
+ getMagicLabelFormats(label, vote, labelType, /* accountId= */ null, /* count= */ count);
+ formats.add(formatLabel(label, vote, count));
+ return formats;
+ }
+
+ /** Get magic label formats corresponding to the {MIN, MAX, ANY} label votes. */
+ private static List<String> getMagicLabelFormats(
String label, short labelVal, Optional<LabelType> labelType, @Nullable Account.Id accountId) {
+ return getMagicLabelFormats(label, labelVal, labelType, accountId, /* count= */ null);
+ }
+
+ /** Get magic label formats corresponding to the {MIN, MAX, ANY} label votes. */
+ private static List<String> getMagicLabelFormats(
+ String label,
+ short labelVal,
+ Optional<LabelType> labelType,
+ @Nullable Account.Id accountId,
+ @Nullable Integer count) {
List<String> labels = new ArrayList<>();
if (labelType.isPresent()) {
if (labelVal == labelType.get().getMaxPositive()) {
- labels.add(formatLabel(label, MagicLabelValue.MAX.name(), accountId));
+ labels.add(formatLabel(label, MagicLabelValue.MAX.name(), accountId, count));
}
if (labelVal == labelType.get().getMaxNegative()) {
- labels.add(formatLabel(label, MagicLabelValue.MIN.name(), accountId));
+ labels.add(formatLabel(label, MagicLabelValue.MIN.name(), accountId, count));
}
}
- labels.add(formatLabel(label, MagicLabelValue.ANY.name(), accountId));
+ labels.add(formatLabel(label, MagicLabelValue.ANY.name(), accountId, count));
return labels;
}
+ private static List<String> getLabelOwnerFormats(
+ PatchSetApproval a, ChangeData cd, Optional<LabelType> labelType) {
+ List<String> allFormats = new ArrayList<>();
+ if (cd.change().getOwner().equals(a.accountId())) {
+ allFormats.add(formatLabel(a.label(), a.value(), ChangeQueryBuilder.OWNER_ACCOUNT_ID));
+ allFormats.addAll(
+ getMagicLabelFormats(
+ a.label(), a.value(), labelType, ChangeQueryBuilder.OWNER_ACCOUNT_ID));
+ }
+ return allFormats;
+ }
+
+ private static List<String> getLabelNonUploaderFormats(
+ PatchSetApproval a, ChangeData cd, Optional<LabelType> labelType) {
+ List<String> allFormats = new ArrayList<>();
+ if (!cd.currentPatchSet().uploader().equals(a.accountId())) {
+ allFormats.add(formatLabel(a.label(), a.value(), ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID));
+ allFormats.addAll(
+ getMagicLabelFormats(
+ a.label(), a.value(), labelType, ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID));
+ }
+ return allFormats;
+ }
+
public static Set<String> getAuthorParts(ChangeData cd) {
return SchemaUtil.getPersonParts(cd.getAuthor());
}
@@ -727,25 +820,33 @@
decodeProtos(field, PatchSetApprovalProtoConverter.INSTANCE)));
public static String formatLabel(String label, int value) {
- return formatLabel(label, value, null);
+ return formatLabel(label, value, /* accountId= */ null, /* count= */ null);
+ }
+
+ public static String formatLabel(String label, int value, @Nullable Integer count) {
+ return formatLabel(label, value, /* accountId= */ null, count);
}
public static String formatLabel(String label, int value, Account.Id accountId) {
+ return formatLabel(label, value, accountId, /* count= */ null);
+ }
+
+ public static String formatLabel(
+ String label, int value, @Nullable Account.Id accountId, @Nullable Integer count) {
return label.toLowerCase()
+ (value >= 0 ? "+" : "")
+ value
- + (accountId != null ? "," + formatAccount(accountId) : "");
+ + (accountId != null ? "," + formatAccount(accountId) : "")
+ + (count != null ? ",count=" + count : "");
}
- public static String formatLabel(String label, String value) {
- return formatLabel(label, value, null);
- }
-
- public static String formatLabel(String label, String value, @Nullable Account.Id accountId) {
+ public static String formatLabel(
+ String label, String value, @Nullable Account.Id accountId, @Nullable Integer count) {
return label.toLowerCase()
+ "="
+ value
- + (accountId != null ? "," + formatAccount(accountId) : "");
+ + (accountId != null ? "," + formatAccount(accountId) : "")
+ + (count != null ? ",count=" + count : "");
}
private static String formatAccount(Account.Id accountId) {
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java b/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
index 63c5297..d57f800 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
@@ -39,6 +39,7 @@
import com.google.gerrit.server.query.change.ChangeDataSource;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ChangeStatusPredicate;
+import com.google.gerrit.server.query.change.IsSubmittablePredicate;
import com.google.gerrit.server.query.change.OrSource;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -182,6 +183,7 @@
private Predicate<ChangeData> rewriteImpl(
Predicate<ChangeData> in, ChangeIndex index, QueryOptions opts, MutableInteger leafTerms)
throws QueryParseException {
+ in = IsSubmittablePredicate.rewrite(in);
if (isIndexPredicate(in, index)) {
if (++leafTerms.value > config.maxTerms()) {
throw new TooManyTermsInQueryException();
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 9339d62..9ff806d 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -183,9 +183,35 @@
new Schema.Builder<ChangeData>().add(V69).add(ChangeField.ATTENTION_SET_USERS_COUNT).build();
/** Added new field {@link ChangeField#UPLOADER}. */
+ @Deprecated
static final Schema<ChangeData> V71 =
new Schema.Builder<ChangeData>().add(V70).add(ChangeField.UPLOADER).build();
+ /** Added new field {@link ChangeField#IS_PURE_REVERT}. */
+ @Deprecated
+ static final Schema<ChangeData> V72 =
+ new Schema.Builder<ChangeData>().add(V71).add(ChangeField.IS_PURE_REVERT).build();
+
+ @Deprecated
+ /** Added new "count=$count" argument to the {@link ChangeField#LABEL} operator. */
+ static final Schema<ChangeData> V73 = schema(V72, false);
+
+ @Deprecated
+ /** Added new field {@link ChangeField#IS_SUBMITTABLE} based on submit requirements. */
+ static final Schema<ChangeData> V74 =
+ new Schema.Builder<ChangeData>().add(V73).add(ChangeField.IS_SUBMITTABLE).build();
+
+ /**
+ * Added new field {@link ChangeField#PREFIX_HASHTAG} and {@link ChangeField#PREFIX_TOPIC} to
+ * allow easier search for topics.
+ */
+ static final Schema<ChangeData> V75 =
+ new Schema.Builder<ChangeData>()
+ .add(V74)
+ .add(ChangeField.PREFIX_HASHTAG)
+ .add(ChangeField.PREFIX_TOPIC)
+ .build();
+
/**
* Name of the change index to be used when contacting index backends or loading configurations.
*/
diff --git a/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java b/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
index 57a2091..b804c4c 100644
--- a/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
+++ b/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
@@ -84,7 +84,7 @@
final DataSource<ChangeData> currSource = source;
final ResultSet<ChangeData> rs = currSource.read();
- return new ResultSet<ChangeData>() {
+ return new ResultSet<>() {
@Override
public Iterator<ChangeData> iterator() {
return Iterables.transform(
diff --git a/java/com/google/gerrit/server/index/group/GroupField.java b/java/com/google/gerrit/server/index/group/GroupField.java
index df90c0d..af74514 100644
--- a/java/com/google/gerrit/server/index/group/GroupField.java
+++ b/java/com/google/gerrit/server/index/group/GroupField.java
@@ -47,8 +47,9 @@
exact("owner_uuid").build(g -> g.getOwnerGroupUUID().get());
/** Timestamp indicating when this group was created. */
+ // TODO(issue-15518): Migrate type for timestamp index fields from Timestamp to Instant
public static final FieldDef<InternalGroup, Timestamp> CREATED_ON =
- timestamp("created_on").build(InternalGroup::getCreatedOn);
+ timestamp("created_on").build(internalGroup -> Timestamp.from(internalGroup.getCreatedOn()));
/** Group name. */
public static final FieldDef<InternalGroup, String> NAME =
diff --git a/java/com/google/gerrit/server/logging/BUILD b/java/com/google/gerrit/server/logging/BUILD
index ee0168c..7204c07 100644
--- a/java/com/google/gerrit/server/logging/BUILD
+++ b/java/com/google/gerrit/server/logging/BUILD
@@ -18,6 +18,5 @@
"//lib/auto:auto-value-annotations",
"//lib/flogger:api",
"//lib/guice",
- "//lib/log:log4j",
],
)
diff --git a/java/com/google/gerrit/server/logging/Metadata.java b/java/com/google/gerrit/server/logging/Metadata.java
index 89b5b46..5cd0e98 100644
--- a/java/com/google/gerrit/server/logging/Metadata.java
+++ b/java/com/google/gerrit/server/logging/Metadata.java
@@ -165,6 +165,8 @@
/** The name of a REST view. */
public abstract Optional<String> restViewName();
+ public abstract Optional<String> submitRequirementName();
+
/** The SHA1 of Git commit. */
public abstract Optional<String> revision();
@@ -375,6 +377,8 @@
public abstract Builder revision(@Nullable String revision);
+ public abstract Builder submitRequirementName(@Nullable String srName);
+
public abstract Builder username(@Nullable String username);
public abstract Metadata build();
diff --git a/java/com/google/gerrit/server/logging/RequestId.java b/java/com/google/gerrit/server/logging/RequestId.java
index 543f0a2..3ae9598 100644
--- a/java/com/google/gerrit/server/logging/RequestId.java
+++ b/java/com/google/gerrit/server/logging/RequestId.java
@@ -61,7 +61,7 @@
h.putLong(Thread.currentThread().getId()).putUnencodedChars(MACHINE_ID);
str =
(resourceId != null ? resourceId + "-" : "")
- + TimeUtil.nowTs().getTime()
+ + TimeUtil.now().toEpochMilli()
+ "-"
+ h.hash().toString().substring(0, 8);
}
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index 0710784..0fc89ba 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -301,7 +301,9 @@
.collect(ImmutableList.toImmutableList());
CommentValidationContext commentValidationCtx =
CommentValidationContext.create(
- cd.change().getChangeId(), cd.change().getProject().get());
+ cd.change().getChangeId(),
+ cd.change().getProject().get(),
+ cd.change().getDest().branch());
ImmutableList<CommentValidationFailure> commentValidationFailures =
PublishCommentUtil.findInvalidComments(
commentValidationCtx, commentValidators, parsedCommentsForValidation);
@@ -311,7 +313,7 @@
}
Op o = new Op(PatchSet.id(cd.getId(), metadata.patchSet), parsedComments, message.id());
- BatchUpdate batchUpdate = buf.create(project, ctx.getUser(), TimeUtil.nowTs());
+ BatchUpdate batchUpdate = buf.create(project, ctx.getUser(), TimeUtil.now());
batchUpdate.addOp(cd.getId(), o);
batchUpdate.execute();
}
@@ -378,8 +380,7 @@
// Get previous approvals from this user
Map<String, Short> approvals = new HashMap<>();
approvalsUtil
- .byPatchSetUser(
- notes, psId, ctx.getAccountId(), ctx.getRevWalk(), ctx.getRepoView().getConfig())
+ .byPatchSetUser(notes, psId, ctx.getAccountId())
.forEach(a -> approvals.put(a.label(), a.value()));
// Fire Gerrit event. Note that approvals can't be granted via email, so old and new approvals
// are always the same here.
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index 1a2e150..63b9c70 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -24,6 +24,7 @@
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;
import com.google.gerrit.entities.NotifyConfig.NotifyType;
import com.google.gerrit.entities.Patch;
@@ -42,6 +43,7 @@
import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.patch.DiffNotAvailableException;
+import com.google.gerrit.server.patch.DiffOptions;
import com.google.gerrit.server.patch.FilePathAdapter;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.patch.filediff.FileDiffOutput;
@@ -51,15 +53,15 @@
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData;
import java.io.IOException;
-import java.sql.Timestamp;
import java.text.MessageFormat;
+import java.time.Instant;
import java.util.Collection;
-import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors;
import org.apache.james.mime4j.dom.field.FieldName;
@@ -86,7 +88,7 @@
protected PatchSet patchSet;
protected PatchSetInfo patchSetInfo;
protected String changeMessage;
- protected Timestamp timestamp;
+ protected Instant timestamp;
protected ProjectState projectState;
protected Set<Account.Id> authors;
@@ -96,17 +98,17 @@
protected ChangeEmail(EmailArguments args, String messageClass, ChangeData changeData) {
super(args, messageClass, changeData.change().getDest());
this.changeData = changeData;
- this.change = changeData.change();
- this.emailOnlyAuthors = false;
- this.emailOnlyAttentionSetIfEnabled = true;
- this.currentAttentionSet = getAttentionSet();
+ change = changeData.change();
+ emailOnlyAuthors = false;
+ emailOnlyAttentionSetIfEnabled = true;
+ currentAttentionSet = getAttentionSet();
}
@Override
public void setFrom(Account.Id id) {
super.setFrom(id);
- /** Is the from user in an email squelching group? */
+ // Is the from user in an email squelching group?
try {
args.permissionBackend.absentUser(id).check(GlobalPermission.EMAIL_REVIEWERS);
} catch (AuthException | PermissionBackendException e) {
@@ -123,7 +125,7 @@
patchSetInfo = psi;
}
- public void setChangeMessage(String cm, Timestamp t) {
+ public void setChangeMessage(String cm, Instant t) {
changeMessage = cm;
timestamp = t;
}
@@ -190,7 +192,7 @@
super.init();
if (timestamp != null) {
- setHeader(FieldName.DATE, new Date(timestamp.getTime()));
+ setHeader(FieldName.DATE, timestamp);
}
setChangeSubjectHeader();
setHeader(MailHeader.CHANGE_ID.fieldName(), "" + change.getKey().get());
@@ -240,11 +242,11 @@
public String getChangeMessageThreadId() {
return "<gerrit."
- + change.getCreatedOn().getTime()
+ + change.getCreatedOn().toEpochMilli()
+ "."
+ change.getKey().get()
+ "@"
- + this.getGerritHost()
+ + getGerritHost()
+ ">";
}
@@ -269,7 +271,8 @@
if (patchSet != null) {
detail.append("---\n");
- Map<String, FileDiffOutput> modifiedFiles = listModifiedFiles();
+ // Sort files by name.
+ TreeMap<String, FileDiffOutput> modifiedFiles = new TreeMap<>(listModifiedFiles());
for (FileDiffOutput fileDiff : modifiedFiles.values()) {
if (fileDiff.newPath().isPresent() && Patch.isMagic(fileDiff.newPath().get())) {
continue;
@@ -319,14 +322,14 @@
}
}
return args.diffOperations.listModifiedFilesAgainstParent(
- change.getProject(), ps.commitId(), /* parentNum= */ 0);
+ change.getProject(), ps.commitId(), /* parentNum= */ 0, DiffOptions.DEFAULTS);
}
/** Get the patch list corresponding to this patch set. */
protected Map<String, FileDiffOutput> listModifiedFiles() throws DiffNotAvailableException {
if (patchSet != null) {
return args.diffOperations.listModifiedFilesAgainstParent(
- change.getProject(), patchSet.commitId(), /* parentNum= */ 0);
+ change.getProject(), patchSet.commitId(), /* parentNum= */ 0, DiffOptions.DEFAULTS);
}
throw new DiffNotAvailableException("no patchSet specified");
}
@@ -507,7 +510,7 @@
soyContext.put("patchSetInfo", patchSetInfoData);
footers.add(MailHeader.CHANGE_ID.withDelimiter() + change.getKey().get());
- footers.add(MailHeader.CHANGE_NUMBER.withDelimiter() + Integer.toString(change.getChangeId()));
+ footers.add(MailHeader.CHANGE_NUMBER.withDelimiter() + change.getChangeId());
footers.add(MailHeader.PATCH_SET.withDelimiter() + patchSet.number());
footers.add(MailHeader.OWNER.withDelimiter() + getNameEmailFor(change.getOwner()));
if (change.getAssignee() != null) {
@@ -559,7 +562,7 @@
try {
attentionSet =
additionsOnly(changeData.attentionSet()).stream()
- .map(a -> a.account())
+ .map(AttentionSetUpdate::account)
.collect(Collectors.toSet());
} catch (StorageException e) {
logger.atWarning().withCause(e).log("Cannot get change attention set");
@@ -595,6 +598,11 @@
try {
ObjectId oldId = modifiedFiles.values().iterator().next().oldCommitId();
ObjectId newId = modifiedFiles.values().iterator().next().newCommitId();
+ if (oldId.equals(ObjectId.zeroId())) {
+ // DiffOperations returns ObjectId.zeroId if newCommit is a root commit, i.e. has no
+ // parents.
+ oldId = null;
+ }
fmt.setRepository(git);
fmt.setDetectRenames(true);
fmt.format(oldId, newId);
@@ -621,7 +629,8 @@
* @param sourceDiff the unified diff that we're converting to the map.
* @return map of 'type' to a line's content.
*/
- protected ImmutableList<ImmutableMap<String, String>> getDiffTemplateData(String sourceDiff) {
+ protected static ImmutableList<ImmutableMap<String, String>> getDiffTemplateData(
+ String sourceDiff) {
ImmutableList.Builder<ImmutableMap<String, String>> result = ImmutableList.builder();
Splitter lineSplitter = Splitter.on(System.getProperty("line.separator"));
for (String diffLine : lineSplitter.split(sourceDiff)) {
diff --git a/java/com/google/gerrit/server/mail/send/CommentFormatter.java b/java/com/google/gerrit/server/mail/send/CommentFormatter.java
index 2590505..f04ce9d 100644
--- a/java/com/google/gerrit/server/mail/send/CommentFormatter.java
+++ b/java/com/google/gerrit/server/mail/send/CommentFormatter.java
@@ -17,9 +17,9 @@
import static com.google.common.base.Strings.isNullOrEmpty;
import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.common.Nullable;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
public class CommentFormatter {
@@ -47,9 +47,9 @@
* @param source The raw, unescaped comment in the Gerrit wiki-like format.
* @return List of block objects, each with unescaped comment content.
*/
- public static List<Block> parse(@Nullable String source) {
+ public static ImmutableList<Block> parse(@Nullable String source) {
if (isNullOrEmpty(source)) {
- return Collections.emptyList();
+ return ImmutableList.of();
}
List<Block> result = new ArrayList<>();
@@ -64,7 +64,7 @@
result.add(makeParagraph(p));
}
}
- return result;
+ return ImmutableList.copyOf(result);
}
/**
diff --git a/java/com/google/gerrit/server/mail/send/CommentSender.java b/java/com/google/gerrit/server/mail/send/CommentSender.java
index 5a7352a..81e4f3e 100644
--- a/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -555,6 +555,6 @@
private String getCommentTimestamp() {
// Grouping is currently done by timestamp.
return MailProcessingUtil.rfcDateformatter.format(
- ZonedDateTime.ofInstant(timestamp.toInstant(), ZoneId.of("UTC")));
+ ZonedDateTime.ofInstant(timestamp, ZoneId.of("UTC")));
}
}
diff --git a/java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java b/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
similarity index 94%
rename from java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java
rename to java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
index aade30f..ad1703d 100644
--- a/java/com/google/gerrit/server/mail/send/MailSoySauceProvider.java
+++ b/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
@@ -19,7 +19,6 @@
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import com.google.inject.Singleton;
import com.google.template.soy.SoyFileSet;
@@ -31,9 +30,13 @@
import java.nio.file.Files;
import java.nio.file.Path;
-/** Configures Soy Sauce object for rendering email templates. */
+/**
+ * Configures and loads Soy Sauce object for rendering email templates.
+ *
+ * <p>It reloads templates each time when {@link #load()} is called.
+ */
@Singleton
-public class MailSoySauceProvider implements Provider<SoySauce> {
+class MailSoySauceLoader {
// Note: will fail to construct the tofu object if this array is empty.
private static final String[] TEMPLATES = {
@@ -90,7 +93,7 @@
private final PluginSetContext<MailSoyTemplateProvider> templateProviders;
@Inject
- MailSoySauceProvider(
+ MailSoySauceLoader(
SitePaths site,
SoyAstCache cache,
PluginSetContext<MailSoyTemplateProvider> templateProviders) {
@@ -99,8 +102,7 @@
this.templateProviders = templateProviders;
}
- @Override
- public SoySauce get() throws ProvisionException {
+ public SoySauce load() {
SoyFileSet.Builder builder = SoyFileSet.builder();
builder.setSoyAstCache(cache);
for (String name : TEMPLATES) {
diff --git a/java/com/google/gerrit/server/mail/send/MailSoySauceModule.java b/java/com/google/gerrit/server/mail/send/MailSoySauceModule.java
new file mode 100644
index 0000000..a3cf3e3
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/send/MailSoySauceModule.java
@@ -0,0 +1,89 @@
+package com.google.gerrit.server.mail.send;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.server.CacheRefreshExecutor;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.inject.Inject;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import com.google.template.soy.jbcsrc.api.SoySauce;
+import java.time.Duration;
+import java.util.concurrent.ExecutionException;
+import javax.inject.Provider;
+
+/**
+ * Provides support for soy templates
+ *
+ * <p>Module loads templates with {@link MailSoySauceLoader} and caches compiled templates. The
+ * cache refreshes automatically, so Gerrit does not need to be restarted if templates are changed.
+ */
+public class MailSoySauceModule extends CacheModule {
+ static final String CACHE_NAME = "soy_sauce_compiled_templates";
+ private static final String SOY_LOADING_CACHE_KEY = "KEY";
+
+ @Override
+ protected void configure() {
+ // Cache stores only a single key-value pair (key is SOY_LOADING_CACHE_KEY). We are using
+ // cache only for it refresh/expire logic.
+ cache(CACHE_NAME, String.class, SoySauce.class)
+ // Cache refreshes a value only on the access (if refreshAfterWrite interval is
+ // passed). While the value is refreshed, cache returns old value.
+ // Adding expireAfterWrite interval prevents cache from returning very old template.
+ .refreshAfterWrite(Duration.ofSeconds(5))
+ .expireAfterWrite(Duration.ofMinutes(1))
+ .loader(SoySauceCacheLoader.class);
+ bind(SoySauce.class).annotatedWith(MailTemplates.class).toProvider(SoySauceProvider.class);
+ }
+
+ @Singleton
+ static class SoySauceProvider implements Provider<SoySauce> {
+ private final LoadingCache<String, SoySauce> templateCache;
+
+ @Inject
+ SoySauceProvider(@Named(CACHE_NAME) LoadingCache<String, SoySauce> templateCache) {
+ this.templateCache = templateCache;
+ }
+
+ @Override
+ public SoySauce get() {
+ try {
+ return templateCache.get(SOY_LOADING_CACHE_KEY);
+ } catch (ExecutionException e) {
+ throw new ProvisionException("Can't get SoySauce from the cache", e);
+ }
+ }
+ }
+
+ @Singleton
+ static class SoySauceCacheLoader extends CacheLoader<String, SoySauce> {
+ private final ListeningExecutorService executor;
+ private final MailSoySauceLoader loader;
+
+ @Inject
+ SoySauceCacheLoader(
+ @CacheRefreshExecutor ListeningExecutorService executor, MailSoySauceLoader loader) {
+ this.executor = executor;
+ this.loader = loader;
+ }
+
+ @Override
+ public SoySauce load(String key) throws Exception {
+ checkArgument(
+ SOY_LOADING_CACHE_KEY.equals(key),
+ "Cache can have only one element with a key '%s'",
+ SOY_LOADING_CACHE_KEY);
+ return loader.load();
+ }
+
+ @Override
+ public ListenableFuture<SoySauce> reload(String key, SoySauce soySauce) {
+ return executor.submit(() -> loader.load());
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/mail/send/NotificationEmail.java b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
index 5ffd928..04b7972 100644
--- a/java/com/google/gerrit/server/mail/send/NotificationEmail.java
+++ b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
@@ -26,6 +26,7 @@
import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.mail.MailHeader;
import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
+import com.google.gerrit.server.mail.send.ProjectWatch.Watchers.WatcherList;
import java.util.HashMap;
import java.util.Map;
@@ -80,11 +81,11 @@
protected abstract Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig);
/** Add users or email addresses to the TO, CC, or BCC list. */
- protected void add(RecipientType type, Watchers.List list) {
- for (Account.Id user : list.accounts) {
+ protected void add(RecipientType type, WatcherList watcherList) {
+ for (Account.Id user : watcherList.accounts) {
add(type, user);
}
- for (Address addr : list.emails) {
+ for (Address addr : watcherList.emails) {
add(type, addr);
}
}
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 286f0c7..bfc1f5b 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -42,9 +42,9 @@
import com.google.template.soy.jbcsrc.api.SoySauce;
import java.net.MalformedURLException;
import java.net.URL;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -67,6 +67,7 @@
private final Set<Account.Id> rcptTo = new HashSet<>();
private final Map<String, EmailHeader> headers;
private final Set<Address> smtpRcptTo = new HashSet<>();
+ private final Set<Address> smtpBccRcptTo = new HashSet<>();
private Address smtpFromAddress;
private StringBuilder textBody;
private StringBuilder htmlBody;
@@ -228,8 +229,13 @@
j.add(address.email());
}
}
- smtpRcptTo.stream().forEach(a -> j.add(a.email()));
- smtpRcptToPlaintextOnly.stream().forEach(a -> j.add(a.email()));
+ // For users who prefer plaintext, this comes at the cost of not being
+ // listed in the multipart To and Cc headers. We work around this by adding
+ // all users to the Reply-To address in both the plaintext and multipart
+ // email. We should exclude any BCC addresses from reply-to, because they should be
+ // invisible to other recipients.
+ Sets.difference(Sets.union(smtpRcptTo, smtpRcptToPlaintextOnly), smtpBccRcptTo).stream()
+ .forEach(a -> j.add(a.email()));
setHeader(FieldName.REPLY_TO, j.toString());
}
@@ -318,7 +324,7 @@
setupSoyContext();
smtpFromAddress = args.fromAddressGenerator.get().from(fromId);
- setHeader(FieldName.DATE, new Date());
+ setHeader(FieldName.DATE, Instant.now());
headers.put(FieldName.FROM, new EmailHeader.AddressList(smtpFromAddress));
headers.put(FieldName.TO, new EmailHeader.AddressList());
headers.put(FieldName.CC, new EmailHeader.AddressList());
@@ -392,7 +398,7 @@
headers.remove(name);
}
- protected void setHeader(String name, Date date) {
+ protected void setHeader(String name, Instant date) {
headers.put(name, new EmailHeader.Date(date));
}
@@ -548,6 +554,8 @@
* Returns whether this email is visible to the given account
*
* @param to account.
+ * @throws PermissionBackendException thrown if checking a permission fails due to an error in the
+ * permission backend
*/
protected boolean isVisibleTo(Account.Id to) throws PermissionBackendException {
return true;
@@ -569,6 +577,7 @@
}
((EmailHeader.AddressList) headers.get(FieldName.TO)).remove(addr.email());
((EmailHeader.AddressList) headers.get(FieldName.CC)).remove(addr.email());
+ smtpBccRcptTo.remove(addr);
}
switch (rt) {
case TO:
@@ -578,6 +587,7 @@
((EmailHeader.AddressList) headers.get(FieldName.CC)).add(addr);
break;
case BCC:
+ smtpBccRcptTo.add(addr);
break;
}
}
diff --git a/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
index 173b121..9299d74 100644
--- a/java/com/google/gerrit/server/mail/send/ProjectWatch.java
+++ b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -30,6 +30,7 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
+import com.google.gerrit.server.mail.send.ProjectWatch.Watchers.WatcherList;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
@@ -111,13 +112,13 @@
}
public static class Watchers {
- static class List {
+ static class WatcherList {
protected final Set<Account.Id> accounts = new HashSet<>();
protected final Set<Address> emails = new HashSet<>();
- private static List union(List... others) {
- List union = new List();
- for (List other : others) {
+ private static WatcherList union(WatcherList... others) {
+ WatcherList union = new WatcherList();
+ for (WatcherList other : others) {
union.accounts.addAll(other.accounts);
union.emails.addAll(other.emails);
}
@@ -125,15 +126,15 @@
}
}
- protected final List to = new List();
- protected final List cc = new List();
- protected final List bcc = new List();
+ protected final WatcherList to = new WatcherList();
+ protected final WatcherList cc = new WatcherList();
+ protected final WatcherList bcc = new WatcherList();
- List all() {
- return List.union(to, cc, bcc);
+ WatcherList all() {
+ return WatcherList.union(to, cc, bcc);
}
- List list(NotifyConfig.Header header) {
+ WatcherList list(NotifyConfig.Header header) {
switch (header) {
case TO:
return to;
@@ -171,7 +172,7 @@
}
}
- private void deliverToMembers(Watchers.List matching, AccountGroup.UUID startUUID) {
+ private void deliverToMembers(WatcherList matching, AccountGroup.UUID startUUID) {
Set<AccountGroup.UUID> seen = new HashSet<>();
List<AccountGroup.UUID> q = new ArrayList<>();
diff --git a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
index d32e6fb..0a721cf 100644
--- a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
@@ -36,10 +36,11 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Writer;
-import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Collections;
-import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -281,9 +282,11 @@
setMissingHeader(hdrs, "Importance", importance);
}
if (expiryDays > 0) {
- Date expiry = new Date(TimeUtil.nowMs() + expiryDays * 24 * 60 * 60 * 1000L);
- setMissingHeader(
- hdrs, "Expiry-Date", new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z").format(expiry));
+ Instant expiry = Instant.ofEpochMilli(TimeUtil.nowMs() + expiryDays * 24 * 60 * 60 * 1000L);
+ DateTimeFormatter fmt =
+ DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss Z")
+ .withZone(ZoneId.systemDefault());
+ setMissingHeader(hdrs, "Expiry-Date", fmt.format(expiry));
}
String encodedBody;
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index 6677490..7efda47 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -28,6 +28,7 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.InternalUser;
import java.io.IOException;
+import java.time.Instant;
import java.util.Date;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Constants;
@@ -45,7 +46,7 @@
protected final Account.Id accountId;
protected final Account.Id realAccountId;
protected final PersonIdent authorIdent;
- protected final Date when;
+ protected final Instant when;
@Nullable private final ChangeNotes notes;
private final Change change;
@@ -55,14 +56,17 @@
private ObjectId result;
boolean rootOnly;
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
AbstractChangeUpdate(
ChangeNotes notes,
CurrentUser user,
PersonIdent serverIdent,
ChangeNoteUtil noteUtil,
- Date when) {
+ Instant when) {
this.noteUtil = noteUtil;
- this.serverIdent = new PersonIdent(serverIdent, when);
+ this.serverIdent = new PersonIdent(serverIdent, Date.from(when));
this.notes = notes;
this.change = notes.getChange();
this.accountId = accountId(user);
@@ -72,6 +76,9 @@
this.when = when;
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
AbstractChangeUpdate(
ChangeNoteUtil noteUtil,
PersonIdent serverIdent,
@@ -80,12 +87,12 @@
Account.Id accountId,
Account.Id realAccountId,
PersonIdent authorIdent,
- Date when) {
+ Instant when) {
checkArgument(
(notes != null && change == null) || (notes == null && change != null),
"exactly one of notes or change required");
this.noteUtil = noteUtil;
- this.serverIdent = new PersonIdent(serverIdent, when);
+ this.serverIdent = new PersonIdent(serverIdent, Date.from(when));
this.notes = notes;
this.change = change != null ? change : notes.getChange();
this.accountId = accountId;
@@ -107,7 +114,7 @@
}
private static PersonIdent ident(
- ChangeNoteUtil noteUtil, PersonIdent serverIdent, CurrentUser u, Date when) {
+ ChangeNoteUtil noteUtil, PersonIdent serverIdent, CurrentUser u, Instant when) {
checkUserType(u);
if (u instanceof IdentifiedUser) {
return noteUtil.newAccountIdIdent(u.asIdentifiedUser().getAccount().id(), when, serverIdent);
@@ -137,7 +144,7 @@
return change;
}
- public Date getWhen() {
+ public Instant getWhen() {
return when;
}
@@ -206,6 +213,9 @@
* deleted.
* @throws IOException if a lower-level error occurred.
*/
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
final ObjectId apply(RevWalk rw, ObjectInserter ins, ObjectId curr) throws IOException {
if (isEmpty()) {
return null;
@@ -226,7 +236,7 @@
return null; // Impl is a no-op.
}
cb.setAuthor(authorIdent);
- cb.setCommitter(new PersonIdent(serverIdent, when));
+ cb.setCommitter(new PersonIdent(serverIdent, Date.from(when)));
setParentCommit(cb, curr);
if (cb.getTreeId() == null) {
if (curr.equals(z)) {
diff --git a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
index 57f6353..5d19205 100644
--- a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -31,9 +31,9 @@
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -60,14 +60,14 @@
@Assisted("effective") Account.Id accountId,
@Assisted("real") Account.Id realAccountId,
PersonIdent authorIdent,
- Date when);
+ Instant when);
ChangeDraftUpdate create(
Change change,
@Assisted("effective") Account.Id accountId,
@Assisted("real") Account.Id realAccountId,
PersonIdent authorIdent,
- Date when);
+ Instant when);
}
@AutoValue
@@ -101,7 +101,7 @@
@Assisted("effective") Account.Id accountId,
@Assisted("real") Account.Id realAccountId,
@Assisted PersonIdent authorIdent,
- @Assisted Date when) {
+ @Assisted Instant when) {
super(noteUtil, serverIdent, notes, null, accountId, realAccountId, authorIdent, when);
this.draftsProject = allUsers;
}
@@ -115,7 +115,7 @@
@Assisted("effective") Account.Id accountId,
@Assisted("real") Account.Id realAccountId,
@Assisted PersonIdent authorIdent,
- @Assisted Date when) {
+ @Assisted Instant when) {
super(noteUtil, serverIdent, null, change, accountId, realAccountId, authorIdent, when);
this.draftsProject = allUsers;
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteJson.java b/java/com/google/gerrit/server/notedb/ChangeNoteJson.java
index 4c41a12..44bb244 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteJson.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteJson.java
@@ -19,6 +19,9 @@
import com.google.gerrit.json.EnumTypeAdapterFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
@@ -26,6 +29,10 @@
import com.google.inject.TypeLiteral;
import java.io.IOException;
import java.sql.Timestamp;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
@Singleton
public class ChangeNoteJson {
@@ -39,6 +46,10 @@
.registerTypeAdapter(
new TypeLiteral<ImmutableList<String>>() {}.getType(),
new ImmutableListAdapter().nullSafe())
+ .registerTypeAdapter(
+ new TypeLiteral<Optional<Boolean>>() {}.getType(),
+ new OptionalBooleanAdapter().nullSafe())
+ .registerTypeAdapter(ObjectId.class, new ObjectIdAdapter())
.setPrettyPrinting()
.create();
}
@@ -47,6 +58,69 @@
return gson;
}
+ static class OptionalBooleanAdapter extends TypeAdapter<Optional<Boolean>> {
+ @Override
+ public void write(JsonWriter out, Optional<Boolean> value) throws IOException {
+ // Serialize the field using the same format used by the AutoValue's default Gson serializer.
+ out.beginObject();
+ out.name("value");
+ if (value.isPresent()) {
+ out.value(value.get());
+ } else {
+ out.nullValue();
+ }
+ out.endObject();
+ }
+
+ @Override
+ public Optional<Boolean> read(JsonReader in) throws IOException {
+ JsonElement parsed = JsonParser.parseReader(in);
+ if (parsed == null) {
+ return Optional.empty();
+ }
+ if (parsed.isJsonObject()) {
+ // If it's not a JSON object, then the boolean value is available directly in the Json
+ // element.
+ parsed = parsed.getAsJsonObject().get("value");
+ }
+ if (parsed == null || parsed.isJsonNull()) {
+ return Optional.empty();
+ }
+ return Optional.of(parsed.getAsBoolean());
+ }
+ }
+
+ /** Json serializer for the {@link ObjectId} class. */
+ static class ObjectIdAdapter extends TypeAdapter<ObjectId> {
+ private static final List<String> legacyFields = Arrays.asList("w1", "w2", "w3", "w4", "w5");
+
+ @Override
+ public void write(JsonWriter out, ObjectId value) throws IOException {
+ out.value(value.name());
+ }
+
+ @Override
+ public ObjectId read(JsonReader in) throws IOException {
+ JsonElement parsed = JsonParser.parseReader(in);
+ if (parsed.isJsonObject() && isJGitFormat(parsed)) {
+ // Some object IDs may have been serialized using the JGit format using the five integers
+ // w1, w2, w3, w4, w5. Detect this case so that we can deserialize properly.
+ int[] raw =
+ legacyFields.stream()
+ .mapToInt(field -> parsed.getAsJsonObject().get(field).getAsInt())
+ .toArray();
+ return ObjectId.fromRaw(raw);
+ }
+ return ObjectId.fromString(parsed.getAsString());
+ }
+
+ /** Return true if the json element contains the JGit serialized format of the Object ID. */
+ private boolean isJGitFormat(JsonElement elem) {
+ JsonObject asObj = elem.getAsJsonObject();
+ return legacyFields.stream().allMatch(field -> asObj.has(field));
+ }
+ }
+
static class ImmutableListAdapter extends TypeAdapter<ImmutableList<String>> {
@Override
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index 28ab711..e9d2f4c 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -15,6 +15,8 @@
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;
@@ -23,7 +25,9 @@
import com.google.inject.Inject;
import java.time.Instant;
import java.util.Date;
+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;
@@ -31,29 +35,30 @@
public class ChangeNoteUtil {
- static final FooterKey FOOTER_ATTENTION = new FooterKey("Attention");
- static final FooterKey FOOTER_ASSIGNEE = new FooterKey("Assignee");
- static final FooterKey FOOTER_BRANCH = new FooterKey("Branch");
- static final FooterKey FOOTER_CHANGE_ID = new FooterKey("Change-id");
- static final FooterKey FOOTER_COMMIT = new FooterKey("Commit");
- static final FooterKey FOOTER_CURRENT = new FooterKey("Current");
- static final FooterKey FOOTER_GROUPS = new FooterKey("Groups");
- static final FooterKey FOOTER_HASHTAGS = new FooterKey("Hashtags");
- static final FooterKey FOOTER_LABEL = new FooterKey("Label");
- static final FooterKey FOOTER_COPIED_LABEL = new FooterKey("Copied-Label");
- static final FooterKey FOOTER_PATCH_SET = new FooterKey("Patch-set");
- static final FooterKey FOOTER_PATCH_SET_DESCRIPTION = new FooterKey("Patch-set-description");
- static final FooterKey FOOTER_PRIVATE = new FooterKey("Private");
- static final FooterKey FOOTER_REAL_USER = new FooterKey("Real-user");
- static final FooterKey FOOTER_STATUS = new FooterKey("Status");
- static final FooterKey FOOTER_SUBJECT = new FooterKey("Subject");
- static final FooterKey FOOTER_SUBMISSION_ID = new FooterKey("Submission-id");
- static final FooterKey FOOTER_SUBMITTED_WITH = new FooterKey("Submitted-with");
- static final FooterKey FOOTER_TOPIC = new FooterKey("Topic");
- static final FooterKey FOOTER_TAG = new FooterKey("Tag");
- static final FooterKey FOOTER_WORK_IN_PROGRESS = new FooterKey("Work-in-progress");
- static final FooterKey FOOTER_REVERT_OF = new FooterKey("Revert-of");
- static final FooterKey FOOTER_CHERRY_PICK_OF = new FooterKey("Cherry-pick-of");
+ 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";
@@ -95,11 +100,15 @@
* Returns a {@link PersonIdent} that contains the account ID, but not the user's name or email
* address.
*/
- public PersonIdent newAccountIdIdent(Account.Id accountId, Date when, PersonIdent serverIdent) {
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
+ public PersonIdent newAccountIdIdent(
+ Account.Id accountId, Instant when, PersonIdent serverIdent) {
return new PersonIdent(
getAccountIdAsUsername(accountId),
getAccountIdAsEmailAddress(accountId),
- when,
+ Date.from(when),
serverIdent.getTimeZone());
}
@@ -245,4 +254,203 @@
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>
+ */
+ public static ParsedPatchSetApproval parseCopiedApproval(String labelLine)
+ throws ConfigInvalidException {
+ try {
+ // Copied approvals can't be explicitly removed. They are removed the same way as non-copied
+ // approvals.
+ checkFooter(!labelLine.startsWith("-"), FOOTER_COPIED_LABEL, labelLine);
+ ParsedPatchSetApproval.Builder rawPatchSetApproval =
+ ParsedPatchSetApproval.builder().footerLine(labelLine).isRemoval(false);
+
+ int tagStart = labelLine.indexOf(":\"");
+ int uuidStart = labelLine.indexOf(", ");
+
+ // 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(0, 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 a1d6b29..bec4b721f 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -66,7 +66,7 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -444,13 +444,7 @@
return approvals;
}
- /**
- * This method is currently used only in tests. TODO(paiking): Use this method to fetch approvals
- * (including copied approvals) instead of computing copied approvals on demand. This will be used
- * by {@code ApprovalCache}.
- *
- * @return all approvals, including copied approvals.
- */
+ /** Gets all approvals, including copied approvals. */
public ImmutableListMultimap<PatchSet.Id, PatchSetApproval> getApprovalsWithCopied() {
if (approvalsWithCopied == null) {
approvalsWithCopied = ImmutableListMultimap.copyOf(state.approvals());
@@ -493,11 +487,19 @@
/**
* Returns the evaluated submit requirements for the change. We only intend to store submit
- * requirements in NoteDb for closed changes, hence the result will be an empty list for active
- * changes, or a list of submit requirements results otherwise. For closed changes, the results
- * represent the state of evaluating submit requirements for this change when it was merged.
+ * requirements in NoteDb for closed changes. For closed changes, the results represent the state
+ * of evaluating submit requirements for this change when it was merged or abandoned.
+ *
+ * @throws UnsupportedOperationException if submit requirements are requested for an open change.
*/
public ImmutableList<SubmitRequirementResult> getSubmitRequirementsResult() {
+ if (state.columns().status().isOpen()) {
+ throw new UnsupportedOperationException(
+ String.format(
+ "Cannot request stored submit requirements"
+ + " for an open change: project = %s, change ID = %d",
+ getProjectName(), state.changeId().get()));
+ }
return state.submitRequirementsResult();
}
@@ -565,7 +567,7 @@
}
/** Returns {@link Optional} value of time when the change was merged. */
- public Optional<Timestamp> getMergedOn() {
+ public Optional<Instant> getMergedOn() {
return Optional.ofNullable(state.mergedOn());
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
index c554ca5..b8b8a2c 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
@@ -61,7 +61,7 @@
.weigher(Weigher.class)
.maximumWeight(10 << 20)
.diskLimit(-1)
- .version(2)
+ .version(3)
.keySerializer(Key.Serializer.INSTANCE)
.valueSerializer(ChangeNotesState.Serializer.INSTANCE);
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 5cf3a64..0d59542 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -40,11 +40,13 @@
import static com.google.gerrit.server.notedb.ChangeNoteUtil.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;
import static java.util.stream.Collectors.joining;
import com.google.common.base.Enums;
import com.google.common.base.Splitter;
import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.ListMultimap;
@@ -70,12 +72,14 @@
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.entities.SubmitRecord.Label.Status;
import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.metrics.Timer0;
import com.google.gerrit.server.AssigneeStatusUpdate;
import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
+import com.google.gerrit.server.notedb.ChangeNoteUtil.ParsedPatchSetApproval;
import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
import com.google.gerrit.server.util.LabelVote;
import java.io.IOException;
@@ -83,6 +87,7 @@
import java.sql.Timestamp;
import java.time.Instant;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -95,6 +100,7 @@
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
+import java.util.stream.Collectors;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.InvalidObjectIdException;
import org.eclipse.jgit.lib.ObjectId;
@@ -107,6 +113,9 @@
class ChangeNotesParser {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final Splitter RULE_SPLITTER = Splitter.on(": ");
+ private static final Splitter HASHTAG_SPLITTER = Splitter.on(",");
+
// Private final members initialized in the constructor.
private final ChangeNoteJson changeNoteJson;
private final NoteDbMetrics metrics;
@@ -116,8 +125,8 @@
// Private final but mutable members initialized in the constructor and filled
// in during the parsing process.
- private final Table<Account.Id, ReviewerStateInternal, Timestamp> reviewers;
- private final Table<Address, ReviewerStateInternal, Timestamp> reviewersByEmail;
+ private final Table<Account.Id, ReviewerStateInternal, Instant> reviewers;
+ private final Table<Address, ReviewerStateInternal, Instant> reviewersByEmail;
private final List<Account.Id> allPastReviewers;
private final List<ReviewerStatusUpdate> reviewerUpdates;
/** Holds only the most recent update per user. Older updates are discarded. */
@@ -142,8 +151,8 @@
private Change.Status status;
private String topic;
private Set<String> hashtags;
- private Timestamp createdOn;
- private Timestamp lastUpdatedOn;
+ private Instant createdOn;
+ private Instant lastUpdatedOn;
private Account.Id ownerId;
private String serverId;
private String changeId;
@@ -164,7 +173,7 @@
// We only set the value once, based on the latest update (the actual value or Optional.empty() if
// the latest record unsets the field).
private Optional<PatchSet.Id> cherryPickOf;
- private Timestamp mergedOn;
+ private Instant mergedOn;
ChangeNotesParser(
Change.Id changeId,
@@ -312,10 +321,81 @@
}
result.put(a.key().patchSetId(), a.build());
}
+ if (status != null && status.isClosed() && !isAnyApprovalCopied(result)) {
+ // If the change is closed, check if there are "submit records" with approvals that do not
+ // exist on the latest patch-set and copy them to the latest patch-set.
+ // We do not invoke this logic if any approval is copied. This is because prior to change
+ // https://gerrit-review.googlesource.com/c/gerrit/+/318135 we used to copy approvals
+ // dynamically (e.g. when requesting the change page). After that change, we started
+ // persisting copied votes in NoteDb, so we don't need to do this back-filling.
+ // Prior to that change (318135), we could've had changes with dynamically copied approvals
+ // that were merged in NoteDb but these approvals do not exist on the latest patch-set, so
+ // we need to back-fill these approvals.
+ PatchSet.Id latestPs = buildCurrentPatchSetId();
+ backFillMissingCopiedApprovalsFromSubmitRecords(result, latestPs).stream()
+ .forEach(a -> result.put(latestPs, a));
+ }
result.keySet().forEach(k -> result.get(k).sort(ChangeNotes.PSA_BY_TIME));
return result;
}
+ /**
+ * Returns patch-set approvals that do not exist on the latest patch-set but for which a submit
+ * record exists in NoteDb when the change was merged.
+ */
+ private List<PatchSetApproval> backFillMissingCopiedApprovalsFromSubmitRecords(
+ ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals, @Nullable PatchSet.Id latestPs) {
+ List<PatchSetApproval> copiedApprovals = new ArrayList<>();
+ if (latestPs == null) {
+ return copiedApprovals;
+ }
+ List<PatchSetApproval> approvalsOnLatestPs = allApprovals.get(latestPs);
+ ListMultimap<Account.Id, PatchSetApproval> approvalsByUser = getApprovalsByUser(allApprovals);
+ List<SubmitRecord.Label> submitRecordLabels =
+ submitRecords.stream()
+ .filter(r -> r.labels != null)
+ .flatMap(r -> r.labels.stream())
+ .filter(label -> Status.OK.equals(label.status) || Status.MAY.equals(label.status))
+ .collect(Collectors.toList());
+ for (SubmitRecord.Label recordLabel : submitRecordLabels) {
+ String labelName = recordLabel.label;
+ Account.Id appliedBy = recordLabel.appliedBy;
+ if (appliedBy == null || labelName == null) {
+ continue;
+ }
+ boolean existsAtLatestPs =
+ approvalsOnLatestPs.stream()
+ .anyMatch(a -> a.accountId().equals(appliedBy) && a.label().equals(labelName));
+ if (existsAtLatestPs) {
+ continue;
+ }
+ // Search for an approval for this label on the max previous patch-set and copy the approval.
+ Collection<PatchSetApproval> userApprovals =
+ approvalsByUser.get(appliedBy).stream()
+ .filter(approval -> approval.label().equals(labelName))
+ .collect(Collectors.toList());
+ if (userApprovals.isEmpty()) {
+ continue;
+ }
+ PatchSetApproval lastApproved =
+ Collections.max(userApprovals, comparingInt(a -> a.patchSetId().get()));
+ copiedApprovals.add(lastApproved.copyWithPatchSet(latestPs));
+ }
+ return copiedApprovals;
+ }
+
+ private boolean isAnyApprovalCopied(ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals) {
+ return allApprovals.values().stream().anyMatch(approval -> approval.copied());
+ }
+
+ private ListMultimap<Account.Id, PatchSetApproval> getApprovalsByUser(
+ ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals) {
+ return allApprovals.values().stream()
+ .collect(
+ ImmutableListMultimap.toImmutableListMultimap(
+ PatchSetApproval::accountId, Function.identity()));
+ }
+
private List<ReviewerStatusUpdate> buildReviewerUpdates() {
List<ReviewerStatusUpdate> result = new ArrayList<>();
HashMap<Account.Id, ReviewerStateInternal> lastState = new HashMap<>();
@@ -333,7 +413,7 @@
}
private void parse(ChangeNotesCommit commit) throws ConfigInvalidException {
- Timestamp commitTimestamp = getCommitTimestamp(commit);
+ Instant commitTimestamp = getCommitTimestamp(commit);
createdOn = commitTimestamp;
parseTag(commit);
@@ -387,7 +467,7 @@
parseSubmission(commit, commitTimestamp);
- if (lastUpdatedOn == null || commitTimestamp.after(lastUpdatedOn)) {
+ if (lastUpdatedOn == null || commitTimestamp.isAfter(lastUpdatedOn)) {
lastUpdatedOn = commitTimestamp;
}
@@ -449,7 +529,7 @@
}
}
- private void parseSubmission(ChangeNotesCommit commit, Timestamp commitTimestamp)
+ private void parseSubmission(ChangeNotesCommit commit, Instant commitTimestamp)
throws ConfigInvalidException {
// Only parse the most recent sumbit commit (there should be exactly one).
if (submissionId == null) {
@@ -532,7 +612,7 @@
}
}
- private void parsePatchSet(PatchSet.Id psId, ObjectId rev, Account.Id accountId, Timestamp ts)
+ private void parsePatchSet(PatchSet.Id psId, ObjectId rev, Account.Id accountId, Instant ts)
throws ConfigInvalidException {
if (accountId == null) {
throw parseException("patch set %s requires an identified user as uploader", psId.get());
@@ -605,7 +685,7 @@
} else if (hashtagsLines.get(0).isEmpty()) {
hashtags = ImmutableSet.of();
} else {
- hashtags = Sets.newHashSet(Splitter.on(',').split(hashtagsLines.get(0)));
+ hashtags = Sets.newHashSet(HASHTAG_SPLITTER.split(hashtagsLines.get(0)));
}
}
@@ -627,7 +707,7 @@
}
}
- private void parseAssigneeUpdates(Timestamp ts, ChangeNotesCommit commit)
+ private void parseAssigneeUpdates(Instant ts, ChangeNotesCommit commit)
throws ConfigInvalidException {
String assigneeValue = parseOneFooter(commit, FOOTER_ASSIGNEE);
if (assigneeValue != null) {
@@ -737,7 +817,7 @@
Account.Id accountId,
Account.Id realAccountId,
ChangeNotesCommit commit,
- Timestamp ts) {
+ Instant ts) {
Optional<String> changeMsgString = getChangeMessageString(commit);
if (!changeMsgString.isPresent()) {
return false;
@@ -783,8 +863,28 @@
for (HumanComment c : e.getValue().getEntities()) {
humanComments.put(e.getKey(), c);
}
- for (SubmitRequirementResult sr : e.getValue().getSubmitRequirementsResult()) {
- submitRequirementResults.add(sr);
+ }
+
+ // Lookup submit requirement results from the revision notes of the last PS that has stored
+ // submit requirements. This is important for cases where the change was abandoned/un-abandoned
+ // multiple times. With each abandon, we store submit requirement results in NoteDb, so we can
+ // end up having stored SRs in many revision notes. We should only return SRs from the last
+ // PS of them.
+ for (PatchSet.Builder ps :
+ patchSets.values().stream()
+ .sorted(comparingInt((PatchSet.Builder p) -> p.id().get()).reversed())
+ .collect(Collectors.toList())) {
+ Optional<ObjectId> maybePsCommitId = ps.commitId();
+ if (!maybePsCommitId.isPresent()) {
+ continue;
+ }
+ ObjectId psCommitId = maybePsCommitId.get();
+ if (rns.containsKey(psCommitId)
+ && rns.get(psCommitId).getSubmitRequirementsResult() != null) {
+ rns.get(psCommitId)
+ .getSubmitRequirementsResult()
+ .forEach(sr -> submitRequirementResults.add(sr));
+ break;
}
}
@@ -801,61 +901,46 @@
}
}
- // Footer example: Copied-Label: <LABEL>=VOTE <Gerrit Account>,<Gerrit Real Account> :"<TAG>"
- // ":<"TAG>"" is optional. <Gerrit Real Account> is also optional, if it was not set.
- // 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).
- private void parseCopiedApproval(PatchSet.Id psId, Timestamp ts, String line)
+ /** Parses copied {@link PatchSetApproval}. */
+ private void parseCopiedApproval(PatchSet.Id psId, Instant ts, String line)
throws ConfigInvalidException {
- // Copied approvals can't be explicitly removed. They are removed the same way as non-copied
- // approvals.
- checkFooter(!line.startsWith("-"), FOOTER_COPIED_LABEL, line);
+ ParsedPatchSetApproval parsedPatchSetApproval = ChangeNoteUtil.parseCopiedApproval(line);
+ checkFooter(
+ parsedPatchSetApproval.accountIdent().isPresent(),
+ FOOTER_COPIED_LABEL,
+ parsedPatchSetApproval.footerLine());
+ PersonIdent accountIdent =
+ RawParseUtils.parsePersonIdent(parsedPatchSetApproval.accountIdent().get());
- Account.Id accountId, realAccountId = null;
- String labelVoteStr;
- String tag = null;
- int s = line.indexOf(' ');
- int tagStart = line.indexOf(":\"");
+ checkFooter(accountIdent != null, FOOTER_COPIED_LABEL, parsedPatchSetApproval.footerLine());
+ Account.Id accountId = parseIdent(accountIdent);
- // The first account is the accountId, and second (if applicable) is the realAccountId.
- try {
- labelVoteStr = line.substring(0, s);
- } catch (StringIndexOutOfBoundsException ex) {
- throw new ConfigInvalidException(ex.getMessage(), ex);
- }
- String[] identities =
- line.substring(s + 1, tagStart == -1 ? line.length() : tagStart).split(",");
- PersonIdent ident = RawParseUtils.parsePersonIdent(identities[0]);
- checkFooter(ident != null, FOOTER_COPIED_LABEL, line);
- accountId = parseIdent(ident);
-
- if (identities.length > 1) {
- PersonIdent realIdent = RawParseUtils.parsePersonIdent(identities[1]);
- checkFooter(realIdent != null, FOOTER_COPIED_LABEL, line);
+ Account.Id realAccountId = null;
+ if (parsedPatchSetApproval.realAccountIdent().isPresent()) {
+ PersonIdent realIdent =
+ RawParseUtils.parsePersonIdent(parsedPatchSetApproval.realAccountIdent().get());
+ checkFooter(realIdent != null, FOOTER_COPIED_LABEL, parsedPatchSetApproval.footerLine());
realAccountId = parseIdent(realIdent);
}
LabelVote l;
try {
- l = LabelVote.parseWithEquals(labelVoteStr);
+ l = LabelVote.parseWithEquals(parsedPatchSetApproval.labelVote());
} catch (IllegalArgumentException e) {
- ConfigInvalidException pe = parseException("invalid %s: %s", FOOTER_COPIED_LABEL, line);
+ ConfigInvalidException pe =
+ parseException(
+ "invalid %s: %s", FOOTER_COPIED_LABEL, parsedPatchSetApproval.footerLine());
pe.initCause(e);
throw pe;
}
- if (tagStart != -1) {
- // tagStart+2 skips ":\"" to parse the actual tag. Tags are in brackets.
- // line.length()-1 skips the last ".
- tag = line.substring(tagStart + 2, line.length() - 1);
- }
-
PatchSetApproval.Builder psa =
PatchSetApproval.builder()
.key(PatchSetApproval.key(psId, accountId, LabelId.create(l.label())))
+ .uuid(parsedPatchSetApproval.uuid().map(PatchSetApproval::uuid))
.value(l.value())
.granted(ts)
- .tag(Optional.ofNullable(tag))
+ .tag(parsedPatchSetApproval.tag())
.copied(true);
if (realAccountId != null) {
psa.realAccountId(realAccountId);
@@ -865,60 +950,46 @@
}
private void parseApproval(
- PatchSet.Id psId, Account.Id accountId, Account.Id realAccountId, Timestamp ts, String line)
+ PatchSet.Id psId, Account.Id accountId, Account.Id realAccountId, Instant ts, String line)
throws ConfigInvalidException {
if (accountId == null) {
throw parseException("patch set %s requires an identified user as uploader", psId.get());
}
PatchSetApproval.Builder psa;
+ ParsedPatchSetApproval parsedPatchSetApproval = ChangeNoteUtil.parseApproval(line);
if (line.startsWith("-")) {
- psa = parseRemoveApproval(psId, accountId, realAccountId, ts, line);
+ psa = parseRemoveApproval(psId, accountId, realAccountId, ts, parsedPatchSetApproval);
} else {
- psa = parseAddApproval(psId, accountId, realAccountId, ts, line);
+ psa = parseAddApproval(psId, accountId, realAccountId, ts, parsedPatchSetApproval);
}
bufferedApprovals.add(psa);
}
+ /** Parses {@link PatchSetApproval} out of the {@link ChangeNoteUtil#FOOTER_LABEL} value. */
private PatchSetApproval.Builder parseAddApproval(
- PatchSet.Id psId, Account.Id committerId, Account.Id realAccountId, Timestamp ts, String line)
+ PatchSet.Id psId,
+ Account.Id committerId,
+ Account.Id realAccountId,
+ Instant ts,
+ ParsedPatchSetApproval parsedPatchSetApproval)
throws ConfigInvalidException {
- // There are potentially 3 accounts involved here:
- // 1. The account from the commit, which is the effective IdentifiedUser
- // that produced the update.
- // 2. The account in the label footer itself, which is used during submit
- // to copy other users' labels to a new patch set.
- // 3. The account in the Real-user footer, indicating that the whole
- // update operation was executed by this user on behalf of the effective
- // user.
- Account.Id effectiveAccountId;
- String labelVoteStr;
- int s = line.indexOf(' ');
- if (s > 0) {
- // Account in the label line (2) becomes the effective ID of the
- // approval. If there is a real user (3) different from the commit user
- // (2), we actually don't store that anywhere in this case; it's more
- // important to record that the real user (3) actually initiated submit.
- labelVoteStr = line.substring(0, s);
- PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(s + 1));
- checkFooter(ident != null, FOOTER_LABEL, line);
- effectiveAccountId = parseIdent(ident);
- } else {
- labelVoteStr = line;
- effectiveAccountId = committerId;
- }
+
+ Account.Id approverId = parseApprover(committerId, parsedPatchSetApproval);
LabelVote l;
try {
- l = LabelVote.parseWithEquals(labelVoteStr);
+ l = LabelVote.parseWithEquals(parsedPatchSetApproval.labelVote());
} catch (IllegalArgumentException e) {
- ConfigInvalidException pe = parseException("invalid %s: %s", FOOTER_LABEL, line);
+ ConfigInvalidException pe =
+ parseException("invalid %s: %s", FOOTER_LABEL, parsedPatchSetApproval.footerLine());
pe.initCause(e);
throw pe;
}
PatchSetApproval.Builder psa =
PatchSetApproval.builder()
- .key(PatchSetApproval.key(psId, effectiveAccountId, LabelId.create(l.label())))
+ .key(PatchSetApproval.key(psId, approverId, LabelId.create(l.label())))
+ .uuid(parsedPatchSetApproval.uuid().map(PatchSetApproval::uuid))
.value(l.value())
.granted(ts)
.tag(Optional.ofNullable(tag));
@@ -930,26 +1001,24 @@
}
private PatchSetApproval.Builder parseRemoveApproval(
- PatchSet.Id psId, Account.Id committerId, Account.Id realAccountId, Timestamp ts, String line)
+ PatchSet.Id psId,
+ Account.Id committerId,
+ Account.Id realAccountId,
+ Instant ts,
+ ParsedPatchSetApproval parsedPatchSetApproval)
throws ConfigInvalidException {
- // See comments in parseAddApproval about the various users involved.
- Account.Id effectiveAccountId;
- String label;
- int s = line.indexOf(' ');
- if (s > 0) {
- label = line.substring(1, s);
- PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(s + 1));
- checkFooter(ident != null, FOOTER_LABEL, line);
- effectiveAccountId = parseIdent(ident);
- } else {
- label = line.substring(1);
- effectiveAccountId = committerId;
- }
+
+ checkFooter(
+ parsedPatchSetApproval.footerLine().startsWith("-"),
+ FOOTER_LABEL,
+ parsedPatchSetApproval.footerLine());
+ Account.Id approverId = parseApprover(committerId, parsedPatchSetApproval);
try {
- LabelType.checkNameInternal(label);
+ LabelType.checkNameInternal(parsedPatchSetApproval.labelVote());
} catch (IllegalArgumentException e) {
- ConfigInvalidException pe = parseException("invalid %s: %s", FOOTER_LABEL, line);
+ ConfigInvalidException pe =
+ parseException("invalid %s: %s", FOOTER_LABEL, parsedPatchSetApproval.footerLine());
pe.initCause(e);
throw pe;
}
@@ -958,7 +1027,9 @@
// needs an actual approval in order to block copying an earlier approval over a later delete.
PatchSetApproval.Builder remove =
PatchSetApproval.builder()
- .key(PatchSetApproval.key(psId, effectiveAccountId, LabelId.create(label)))
+ .key(
+ PatchSetApproval.key(
+ psId, approverId, LabelId.create(parsedPatchSetApproval.labelVote())))
.value(0)
.granted(ts);
if (!Objects.equals(realAccountId, committerId)) {
@@ -968,6 +1039,30 @@
return remove;
}
+ /**
+ * Identifies the {@link com.google.gerrit.entities.Account.Id} that issued the vote.
+ *
+ * <p>There are potentially 3 accounts involved here: 1. The account from the commit, which is the
+ * effective IdentifiedUser that produced the update. 2. The account in the label footer itself,
+ * which is used during submit to copy other users' labels to a new patch set. 3. The account in
+ * the Real-user footer, indicating that the whole update operation was executed by this user on
+ * behalf of the effective user.
+ */
+ private Account.Id parseApprover(
+ Account.Id committerId, ParsedPatchSetApproval parsedPatchSetApproval)
+ throws ConfigInvalidException {
+ Account.Id effectiveAccountId;
+ if (parsedPatchSetApproval.accountIdent().isPresent()) {
+ PersonIdent ident =
+ RawParseUtils.parsePersonIdent(parsedPatchSetApproval.accountIdent().get());
+ checkFooter(ident != null, FOOTER_LABEL, parsedPatchSetApproval.footerLine());
+ effectiveAccountId = parseIdent(ident);
+ } else {
+ effectiveAccountId = committerId;
+ }
+ return effectiveAccountId;
+ }
+
private void parseSubmitRecords(List<String> lines) throws ConfigInvalidException {
SubmitRecord rec = null;
@@ -986,7 +1081,7 @@
} else {
checkFooter(rec != null, FOOTER_SUBMITTED_WITH, line);
if (line.startsWith("Rule-Name: ")) {
- String ruleName = line.split(": ")[1];
+ String ruleName = RULE_SPLITTER.splitToList(line).get(1);
rec.ruleName = ruleName;
continue;
}
@@ -1023,7 +1118,7 @@
return parseIdent(a);
}
- private void parseReviewer(Timestamp ts, ReviewerStateInternal state, String line)
+ private void parseReviewer(Instant ts, ReviewerStateInternal state, String line)
throws ConfigInvalidException {
PersonIdent ident = RawParseUtils.parsePersonIdent(line);
if (ident == null) {
@@ -1036,7 +1131,7 @@
}
}
- private void parseReviewerByEmail(Timestamp ts, ReviewerStateInternal state, String line)
+ private void parseReviewerByEmail(Instant ts, ReviewerStateInternal state, String line)
throws ConfigInvalidException {
Address adr;
try {
@@ -1150,15 +1245,18 @@
* @param commit the commit to return commit time.
* @return the timestamp when the commit was applied.
*/
- private Timestamp getCommitTimestamp(ChangeNotesCommit commit) {
- return new Timestamp(commit.getCommitterIdent().getWhen().getTime());
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
+ private Instant getCommitTimestamp(ChangeNotesCommit commit) {
+ return commit.getCommitterIdent().getWhen().toInstant();
}
private void pruneReviewers() {
- Iterator<Table.Cell<Account.Id, ReviewerStateInternal, Timestamp>> rit =
+ Iterator<Table.Cell<Account.Id, ReviewerStateInternal, Instant>> rit =
reviewers.cellSet().iterator();
while (rit.hasNext()) {
- Table.Cell<Account.Id, ReviewerStateInternal, Timestamp> e = rit.next();
+ Table.Cell<Account.Id, ReviewerStateInternal, Instant> e = rit.next();
if (e.getColumnKey() == ReviewerStateInternal.REMOVED) {
rit.remove();
}
@@ -1166,10 +1264,10 @@
}
private void pruneReviewersByEmail() {
- Iterator<Table.Cell<Address, ReviewerStateInternal, Timestamp>> rit =
+ Iterator<Table.Cell<Address, ReviewerStateInternal, Instant>> rit =
reviewersByEmail.cellSet().iterator();
while (rit.hasNext()) {
- Table.Cell<Address, ReviewerStateInternal, Timestamp> e = rit.next();
+ Table.Cell<Address, ReviewerStateInternal, Instant> e = rit.next();
if (e.getColumnKey() == ReviewerStateInternal.REMOVED) {
rit.remove();
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index 4d6b9cf..b0079d7 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -65,7 +65,6 @@
import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
import com.google.gerrit.server.index.change.ChangeField.StoredSubmitRecord;
import com.google.gson.Gson;
-import java.sql.Timestamp;
import java.time.Instant;
import java.util.List;
import java.util.Map;
@@ -103,8 +102,8 @@
ObjectId metaId,
Change.Id changeId,
Change.Key changeKey,
- Timestamp createdOn,
- Timestamp lastUpdatedOn,
+ Instant createdOn,
+ Instant lastUpdatedOn,
Account.Id owner,
String serverId,
String branch,
@@ -136,7 +135,7 @@
@Nullable Change.Id revertOf,
@Nullable PatchSet.Id cherryPickOf,
int updateCount,
- @Nullable Timestamp mergedOn) {
+ @Nullable Instant mergedOn) {
requireNonNull(
metaId,
() ->
@@ -203,9 +202,9 @@
abstract Change.Key changeKey();
- abstract Timestamp createdOn();
+ abstract Instant createdOn();
- abstract Timestamp lastUpdatedOn();
+ abstract Instant lastUpdatedOn();
abstract Account.Id owner();
@@ -249,9 +248,9 @@
abstract Builder changeKey(Change.Key changeKey);
- abstract Builder createdOn(Timestamp createdOn);
+ abstract Builder createdOn(Instant createdOn);
- abstract Builder lastUpdatedOn(Timestamp lastUpdatedOn);
+ abstract Builder lastUpdatedOn(Instant lastUpdatedOn);
abstract Builder owner(Account.Id owner);
@@ -334,7 +333,7 @@
abstract int updateCount();
@Nullable
- abstract Timestamp mergedOn();
+ abstract Instant mergedOn();
Change newChange(Project.NameKey project) {
ChangeColumns c = requireNonNull(columns(), "columns are required");
@@ -456,7 +455,7 @@
abstract Builder updateCount(int updateCount);
- abstract Builder mergedOn(Timestamp mergedOn);
+ abstract Builder mergedOn(Instant mergedOn);
abstract ChangeNotesState build();
}
@@ -536,7 +535,7 @@
SubmitRequirementProtoConverter.INSTANCE.toProto(sr)));
b.setUpdateCount(object.updateCount());
if (object.mergedOn() != null) {
- b.setMergedOnMillis(object.mergedOn().getTime());
+ b.setMergedOnMillis(object.mergedOn().toEpochMilli());
b.setHasMergedOn(true);
}
@@ -547,8 +546,8 @@
ChangeColumnsProto.Builder b =
ChangeColumnsProto.newBuilder()
.setChangeKey(cols.changeKey().get())
- .setCreatedOnMillis(cols.createdOn().getTime())
- .setLastUpdatedOnMillis(cols.lastUpdatedOn().getTime())
+ .setCreatedOnMillis(cols.createdOn().toEpochMilli())
+ .setLastUpdatedOnMillis(cols.lastUpdatedOn().toEpochMilli())
.setOwner(cols.owner().get())
.setBranch(cols.branch());
if (cols.currentPatchSetId() != null) {
@@ -581,26 +580,26 @@
}
private static ReviewerSetEntryProto toReviewerSetEntry(
- Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> c) {
+ Table.Cell<ReviewerStateInternal, Account.Id, Instant> c) {
return ReviewerSetEntryProto.newBuilder()
.setState(REVIEWER_STATE_CONVERTER.reverse().convert(c.getRowKey()))
.setAccountId(c.getColumnKey().get())
- .setTimestampMillis(c.getValue().getTime())
+ .setTimestampMillis(c.getValue().toEpochMilli())
.build();
}
private static ReviewerByEmailSetEntryProto toReviewerByEmailSetEntry(
- Table.Cell<ReviewerStateInternal, Address, Timestamp> c) {
+ Table.Cell<ReviewerStateInternal, Address, Instant> c) {
return ReviewerByEmailSetEntryProto.newBuilder()
.setState(REVIEWER_STATE_CONVERTER.reverse().convert(c.getRowKey()))
.setAddress(c.getColumnKey().toHeaderString())
- .setTimestampMillis(c.getValue().getTime())
+ .setTimestampMillis(c.getValue().toEpochMilli())
.build();
}
private static ReviewerStatusUpdateProto toReviewerStatusUpdateProto(ReviewerStatusUpdate u) {
return ReviewerStatusUpdateProto.newBuilder()
- .setTimestampMillis(u.date().getTime())
+ .setTimestampMillis(u.date().toEpochMilli())
.setUpdatedBy(u.updatedBy().get())
.setReviewer(u.reviewer().get())
.setState(REVIEWER_STATE_CONVERTER.reverse().convert(u.state()))
@@ -620,7 +619,7 @@
private static AssigneeStatusUpdateProto toAssigneeStatusUpdateProto(AssigneeStatusUpdate u) {
AssigneeStatusUpdateProto.Builder builder =
AssigneeStatusUpdateProto.newBuilder()
- .setTimestampMillis(u.date().getTime())
+ .setTimestampMillis(u.date().toEpochMilli())
.setUpdatedBy(u.updatedBy().get())
.setHasCurrentAssignee(u.currentAssignee().isPresent());
@@ -678,7 +677,8 @@
.map(sr -> SubmitRequirementProtoConverter.INSTANCE.fromProto(sr))
.collect(toImmutableList()))
.updateCount(proto.getUpdateCount())
- .mergedOn(proto.getHasMergedOn() ? new Timestamp(proto.getMergedOnMillis()) : null);
+ .mergedOn(
+ proto.getHasMergedOn() ? Instant.ofEpochMilli(proto.getMergedOnMillis()) : null);
return b.build();
}
@@ -686,8 +686,8 @@
ChangeColumns.Builder b =
ChangeColumns.builder()
.changeKey(Change.key(proto.getChangeKey()))
- .createdOn(new Timestamp(proto.getCreatedOnMillis()))
- .lastUpdatedOn(new Timestamp(proto.getLastUpdatedOnMillis()))
+ .createdOn(Instant.ofEpochMilli(proto.getCreatedOnMillis()))
+ .lastUpdatedOn(Instant.ofEpochMilli(proto.getLastUpdatedOnMillis()))
.owner(Account.id(proto.getOwner()))
.branch(proto.getBranch());
if (proto.getHasCurrentPatchSetId()) {
@@ -719,26 +719,25 @@
}
private static ReviewerSet toReviewerSet(List<ReviewerSetEntryProto> protos) {
- ImmutableTable.Builder<ReviewerStateInternal, Account.Id, Timestamp> b =
+ ImmutableTable.Builder<ReviewerStateInternal, Account.Id, Instant> b =
ImmutableTable.builder();
for (ReviewerSetEntryProto e : protos) {
b.put(
REVIEWER_STATE_CONVERTER.convert(e.getState()),
Account.id(e.getAccountId()),
- new Timestamp(e.getTimestampMillis()));
+ Instant.ofEpochMilli(e.getTimestampMillis()));
}
return ReviewerSet.fromTable(b.build());
}
private static ReviewerByEmailSet toReviewerByEmailSet(
List<ReviewerByEmailSetEntryProto> protos) {
- ImmutableTable.Builder<ReviewerStateInternal, Address, Timestamp> b =
- ImmutableTable.builder();
+ ImmutableTable.Builder<ReviewerStateInternal, Address, Instant> b = ImmutableTable.builder();
for (ReviewerByEmailSetEntryProto e : protos) {
b.put(
REVIEWER_STATE_CONVERTER.convert(e.getState()),
Address.parse(e.getAddress()),
- new Timestamp(e.getTimestampMillis()));
+ Instant.ofEpochMilli(e.getTimestampMillis()));
}
return ReviewerByEmailSet.fromTable(b.build());
}
@@ -749,7 +748,7 @@
for (ReviewerStatusUpdateProto proto : protos) {
b.add(
ReviewerStatusUpdate.create(
- new Timestamp(proto.getTimestampMillis()),
+ Instant.ofEpochMilli(proto.getTimestampMillis()),
Account.id(proto.getUpdatedBy()),
Account.id(proto.getReviewer()),
REVIEWER_STATE_CONVERTER.convert(proto.getState())));
@@ -791,7 +790,7 @@
for (AssigneeStatusUpdateProto proto : protos) {
b.add(
AssigneeStatusUpdate.create(
- new Timestamp(proto.getTimestampMillis()),
+ Instant.ofEpochMilli(proto.getTimestampMillis()),
Account.id(proto.getUpdatedBy()),
proto.getHasCurrentAssignee()
? Optional.of(Account.id(proto.getCurrentAssignee()))
diff --git a/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java b/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java
index 44475db..6d49fc8 100644
--- a/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java
+++ b/java/com/google/gerrit/server/notedb/ChangeRevisionNote.java
@@ -17,6 +17,8 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.HumanComment;
import com.google.gerrit.entities.SubmitRequirementResult;
import java.io.ByteArrayInputStream;
@@ -33,18 +35,29 @@
/** Implements the parsing of comment data, handling JSON decoding and push certificates. */
class ChangeRevisionNote extends RevisionNote<HumanComment> {
private final ChangeNoteJson noteJson;
- private final HumanComment.Status status;
+ private final Comment.Status status;
private String pushCert;
- private ImmutableList<SubmitRequirementResult> submitRequirementsResult;
+ /**
+ * Submit requirement results stored in this revision note. If null, then no SRs were stored in
+ * the revision note . Otherwise, there were stored SRs in this revision note. The list could be
+ * empty, meaning that no SRs were configured for the project.
+ */
+ @Nullable private ImmutableList<SubmitRequirementResult> submitRequirementsResult;
ChangeRevisionNote(
- ChangeNoteJson noteJson, ObjectReader reader, ObjectId noteId, HumanComment.Status status) {
+ ChangeNoteJson noteJson, ObjectReader reader, ObjectId noteId, Comment.Status status) {
super(reader, noteId);
this.noteJson = noteJson;
this.status = status;
}
+ /**
+ * Returns null if no submit requirements were stored in the revision note. Otherwise, this method
+ * returns a list of submit requirements, which can probably be empty if there were no SRs
+ * configured for the project at the time when the SRs were stored.
+ */
+ @Nullable
public ImmutableList<SubmitRequirementResult> getSubmitRequirementsResult() {
checkParsed();
return submitRequirementsResult;
@@ -69,7 +82,7 @@
}
this.submitRequirementsResult =
data.submitRequirementResults == null
- ? ImmutableList.of()
+ ? null
: ImmutableList.copyOf(data.submitRequirementResults);
return data.comments;
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 5acea1b..3d7ae10 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -52,6 +52,7 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
import com.google.common.collect.Table;
import com.google.common.collect.Table.Cell;
import com.google.common.collect.TreeBasedTable;
@@ -63,6 +64,7 @@
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.HumanComment;
import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RobotComment;
@@ -74,6 +76,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.ServiceUserClassifier;
+import com.google.gerrit.server.approval.PatchSetApprovalUuidGenerator;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.util.AttentionSetUtil;
import com.google.gerrit.server.util.LabelVote;
@@ -81,10 +84,10 @@
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
-import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
@@ -118,10 +121,10 @@
*/
public class ChangeUpdate extends AbstractChangeUpdate {
public interface Factory {
- ChangeUpdate create(ChangeNotes notes, CurrentUser user, Date when);
+ ChangeUpdate create(ChangeNotes notes, CurrentUser user, Instant when);
ChangeUpdate create(
- ChangeNotes notes, CurrentUser user, Date when, Comparator<String> labelNameComparator);
+ ChangeNotes notes, CurrentUser user, Instant when, Comparator<String> labelNameComparator);
}
private final NoteDbUpdateManager.Factory updateManagerFactory;
@@ -129,13 +132,13 @@
private final RobotCommentUpdate.Factory robotCommentUpdateFactory;
private final DeleteCommentRewriter.Factory deleteCommentRewriterFactory;
private final ServiceUserClassifier serviceUserClassifier;
+ private final PatchSetApprovalUuidGenerator patchSetApprovalUuidGenerator;
private final Table<String, Account.Id, Optional<Short>> approvals;
private final List<PatchSetApproval> copiedApprovals = new ArrayList<>();
private final Map<Account.Id, ReviewerStateInternal> reviewers = new LinkedHashMap<>();
private final Map<Address, ReviewerStateInternal> reviewersByEmail = new LinkedHashMap<>();
private final List<HumanComment> comments = new ArrayList<>();
- private final List<SubmitRequirementResult> submitRequirementResults = new ArrayList<>();
private String commitSubject;
private String subject;
@@ -169,6 +172,7 @@
private RobotCommentUpdate robotCommentUpdate;
private DeleteCommentRewriter deleteCommentRewriter;
private DeleteChangeMessageRewriter deleteChangeMessageRewriter;
+ private List<SubmitRequirementResult> submitRequirementResults;
@AssistedInject
private ChangeUpdate(
@@ -179,9 +183,10 @@
DeleteCommentRewriter.Factory deleteCommentRewriterFactory,
ProjectCache projectCache,
ServiceUserClassifier serviceUserClassifier,
+ PatchSetApprovalUuidGenerator patchSetApprovalUuidGenerator,
@Assisted ChangeNotes notes,
@Assisted CurrentUser user,
- @Assisted Date when,
+ @Assisted Instant when,
ChangeNoteUtil noteUtil) {
this(
serverIdent,
@@ -190,6 +195,7 @@
robotCommentUpdateFactory,
deleteCommentRewriterFactory,
serviceUserClassifier,
+ patchSetApprovalUuidGenerator,
notes,
user,
when,
@@ -214,9 +220,10 @@
RobotCommentUpdate.Factory robotCommentUpdateFactory,
DeleteCommentRewriter.Factory deleteCommentRewriterFactory,
ServiceUserClassifier serviceUserClassifier,
+ PatchSetApprovalUuidGenerator patchSetApprovalUuidGenerator,
@Assisted ChangeNotes notes,
@Assisted CurrentUser user,
- @Assisted Date when,
+ @Assisted Instant when,
@Assisted Comparator<String> labelNameComparator,
ChangeNoteUtil noteUtil) {
super(notes, user, serverIdent, noteUtil, when);
@@ -225,6 +232,7 @@
this.robotCommentUpdateFactory = robotCommentUpdateFactory;
this.deleteCommentRewriterFactory = deleteCommentRewriterFactory;
this.serviceUserClassifier = serviceUserClassifier;
+ this.patchSetApprovalUuidGenerator = patchSetApprovalUuidGenerator;
this.approvals = approvals(labelNameComparator);
}
@@ -319,10 +327,13 @@
}
public void putSubmitRequirementResults(Collection<SubmitRequirementResult> rs) {
+ if (submitRequirementResults == null) {
+ submitRequirementResults = new ArrayList<>();
+ }
submitRequirementResults.addAll(rs);
}
- public void putComment(HumanComment.Status status, HumanComment c) {
+ public void putComment(Comment.Status status, HumanComment c) {
verifyComment(c);
createDraftUpdateIfNull();
if (status == HumanComment.Status.DRAFT) {
@@ -508,7 +519,7 @@
/** Returns the tree id for the updated tree */
private ObjectId storeRevisionNotes(RevWalk rw, ObjectInserter inserter, ObjectId curr)
throws ConfigInvalidException, IOException {
- if (submitRequirementResults.isEmpty() && comments.isEmpty() && pushCert == null) {
+ if (submitRequirementResults == null && comments.isEmpty() && pushCert == null) {
return null;
}
RevisionNoteMap<ChangeRevisionNote> rnm = getRevisionNoteMap(rw, curr);
@@ -518,8 +529,16 @@
c.tag = tag;
cache.get(c.getCommitId()).putComment(c);
}
- for (SubmitRequirementResult sr : submitRequirementResults) {
- cache.get(sr.patchSetCommitId()).putSubmitRequirementResult(sr);
+ if (submitRequirementResults != null) {
+ if (submitRequirementResults.isEmpty()) {
+ ObjectId latestPsCommitId =
+ Iterables.getLast(getNotes().getPatchSets().values()).commitId();
+ cache.get(latestPsCommitId).createEmptySubmitRequirementResults();
+ } else {
+ for (SubmitRequirementResult sr : submitRequirementResults) {
+ cache.get(sr.patchSetCommitId()).putSubmitRequirementResult(sr);
+ }
+ }
}
if (pushCert != null) {
checkState(commit != null);
@@ -630,12 +649,12 @@
deleteCommentRewriter == null && deleteChangeMessageRewriter == null,
"cannot update and rewrite ref in one BatchUpdate");
- int ps = psId != null ? psId.get() : getChange().currentPatchSetId().get();
+ PatchSet.Id patchSetId = psId != null ? psId : getChange().currentPatchSetId();
StringBuilder msg = new StringBuilder();
if (commitSubject != null) {
msg.append(commitSubject);
} else {
- msg.append("Update patch set ").append(ps);
+ msg.append("Update patch set ").append(patchSetId.get());
}
msg.append("\n\n");
@@ -644,7 +663,7 @@
msg.append("\n\n");
}
- addPatchSetFooter(msg, ps);
+ addPatchSetFooter(msg, patchSetId);
if (currentPatchSet) {
addFooter(msg, FOOTER_CURRENT, Boolean.TRUE);
@@ -718,7 +737,7 @@
}
for (Table.Cell<String, Account.Id, Optional<Short>> c : approvals.cellSet()) {
- addLabelFooter(msg, c);
+ addLabelFooter(msg, c, patchSetId);
}
for (PatchSetApproval patchSetApproval : copiedApprovals) {
addCopiedLabelFooter(msg, patchSetApproval);
@@ -802,17 +821,25 @@
return cb;
}
- private void addLabelFooter(StringBuilder msg, Cell<String, Account.Id, Optional<Short>> c) {
+ private void addLabelFooter(
+ StringBuilder msg, Cell<String, Account.Id, Optional<Short>> c, PatchSet.Id patchSetId) {
addFooter(msg, FOOTER_LABEL);
+ String label = c.getRowKey();
+ Account.Id reviewerId = c.getColumnKey();
// Label names/values are safe to append without sanitizing.
- if (!c.getValue().isPresent()) {
- msg.append('-').append(c.getRowKey());
+ boolean isRemoval = !c.getValue().isPresent();
+ if (isRemoval) {
+ msg.append('-').append(label);
+ // Since vote removals do not need to be referenced, e.g. by the copy approvals, they do not
+ // require a UUID.
} else {
- msg.append(LabelVote.create(c.getRowKey(), c.getValue().get()).formatWithEquals());
+ short value = c.getValue().get();
+ msg.append(LabelVote.create(label, c.getValue().get()).formatWithEquals());
+ msg.append(", ");
+ msg.append(patchSetApprovalUuidGenerator.get(patchSetId, reviewerId, label, value, when));
}
- Account.Id id = c.getColumnKey();
- if (!id.equals(getAccountId())) {
- noteUtil.appendAccountIdIdentString(msg.append(' '), id);
+ if (!reviewerId.equals(getAccountId())) {
+ noteUtil.appendAccountIdIdentString(msg.append(' '), reviewerId);
}
msg.append('\n');
}
@@ -826,6 +853,11 @@
// Label names/values are safe to append without sanitizing.
msg.append(
LabelVote.create(patchSetApproval.label(), patchSetApproval.value()).formatWithEquals());
+ // Might be copied from the vote that was generated before UUID was introduced.
+ if (patchSetApproval.uuid().isPresent()) {
+ msg.append(", ");
+ msg.append(patchSetApproval.uuid().get());
+ }
Account.Id id = patchSetApproval.accountId();
noteUtil.appendAccountIdIdentString(msg.append(' '), id);
@@ -840,6 +872,7 @@
if (patchSetApproval.tag().isPresent()) {
msg.append(":\"" + sanitizeFooter(patchSetApproval.tag().get()) + "\"");
}
+
msg.append('\n');
}
@@ -1021,8 +1054,8 @@
ignoreFurtherAttentionSetUpdates = true;
}
- private void addPatchSetFooter(StringBuilder sb, int ps) {
- addFooter(sb, FOOTER_PATCH_SET).append(ps);
+ private void addPatchSetFooter(StringBuilder sb, PatchSet.Id ps) {
+ addFooter(sb, FOOTER_PATCH_SET).append(ps.get());
if (psState != null) {
sb.append(" (").append(psState.name().toLowerCase()).append(')');
}
diff --git a/java/com/google/gerrit/server/notedb/CommitRewriter.java b/java/com/google/gerrit/server/notedb/CommitRewriter.java
index 338b984..24c4d6d 100644
--- a/java/com/google/gerrit/server/notedb/CommitRewriter.java
+++ b/java/com/google/gerrit/server/notedb/CommitRewriter.java
@@ -25,6 +25,7 @@
import static com.google.gerrit.server.util.AccountTemplateUtil.ACCOUNT_TEMPLATE_REGEX;
import com.google.auto.value.AutoValue;
+import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@@ -110,6 +111,8 @@
public class CommitRewriter {
/** Options to run {@link #backfillProject}. */
public static class RunOptions implements Serializable {
+ private static final long serialVersionUID = 1L;
+
/** Whether to rewrite the commit history or only find refs that need to be fixed. */
public boolean dryRun = true;
/**
@@ -123,10 +126,9 @@
/** Max number of refs to update in a single {@link BatchRefUpdate}. */
public int maxRefsInBatch = 10000;
/**
- * Max number of refs to fix by a single {@link RefsUpdate#backfillProject} run. Since second
- * run on the same set of refs is a no-op, running with this option in a loop will eventually
- * fix all refs. Number of executed {@link BatchRefUpdate} depends on {@link #maxRefsInBatch}
- * option.
+ * Max number of refs to fix by a single {@link RefsUpdate} run. Since the second run on the
+ * same set of refs is a no-op, running with this option in a loop will eventually fix all refs.
+ * The number of executed {@link BatchRefUpdate} depends on {@link #maxRefsInBatch} option.
*/
public int maxRefsToUpdate = 50000;
}
@@ -227,6 +229,8 @@
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final Splitter COMMIT_MESSAGE_SPLITTER = Splitter.onPattern("\\r?\\n");
+
private final ChangeNotes.Factory changeNotesFactory;
private final AccountCache accountCache;
private final DiffAlgorithm diffAlgorithm = new HistogramDiff();
@@ -263,6 +267,8 @@
BackfillResult result = new BackfillResult();
result.ok = true;
int refsInUpdate = 0;
+
+ @SuppressWarnings("resource")
RefsUpdate refsUpdate = null;
try {
for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_CHANGES)) {
@@ -474,7 +480,7 @@
}
detailedVerificationStatus.append("Commit author:\n");
detailedVerificationStatus.append(fixedAuthorIdent.toString());
- logger.atWarning().log(detailedVerificationStatus.toString());
+ logger.atWarning().log("%s", detailedVerificationStatus);
}
}
boolean needsFix =
@@ -571,6 +577,9 @@
}
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
private boolean verifyPersonIdent(PersonIdent newIdent, PersonIdent originalIdent) {
return newIdent.getTimeZoneOffset() == originalIdent.getTimeZoneOffset()
&& newIdent.getWhen().getTime() == originalIdent.getWhen().getTime()
@@ -687,15 +696,16 @@
|| !originalChangeMessage.startsWith(REMOVED_VOTES_CHANGE_MESSAGE_START)) {
return Optional.empty();
}
- String[] lines = originalChangeMessage.split("\\r?\\n");
+ List<String> lines = COMMIT_MESSAGE_SPLITTER.splitToList(originalChangeMessage);
StringBuilder fixedLines = new StringBuilder();
boolean anyFixed = false;
- for (int i = 1; i < lines.length; i++) {
- if (lines[i].isEmpty()) {
+ for (int i = 1; i < lines.size(); i++) {
+ String line = lines.get(i);
+ if (line.isEmpty()) {
continue;
}
- Matcher matcher = REMOVED_VOTES_CHANGE_MESSAGE_PATTERN.matcher(lines[i]);
- String replacementLine = lines[i];
+ Matcher matcher = REMOVED_VOTES_CHANGE_MESSAGE_PATTERN.matcher(line);
+ String replacementLine = line;
if (matcher.matches() && !NON_REPLACE_ACCOUNT_PATTERN.matcher(matcher.group(2)).matches()) {
anyFixed = true;
Optional<String> reviewerReplacement =
@@ -766,7 +776,7 @@
// Pre fix, try to replace with something meaningful.
// Retrieve reviewer accounts from cache and try to match by their name.
onAddReviewerMatcher.reset();
- StringBuffer sb = new StringBuffer();
+ StringBuilder sb = new StringBuilder();
while (onAddReviewerMatcher.find()) {
String reviewerName = normalizeOnCodeOwnerAddReviewerMatch(onAddReviewerMatcher.group(1));
Optional<String> replacementName =
@@ -943,7 +953,8 @@
continue;
}
} else if (footerKey.equalsIgnoreCase(FOOTER_LABEL.getName())) {
- int voterIdentStart = footerValue.indexOf(' ');
+ int uuidStart = footerValue.indexOf(", ");
+ int voterIdentStart = footerValue.indexOf(' ', uuidStart != -1 ? uuidStart + 2 : 0);
FixIdentResult fixedVoter = null;
if (voterIdentStart > 0) {
String originalIdentString = footerValue.substring(voterIdentStart + 1);
@@ -1176,7 +1187,8 @@
// Filter further so we match both email & name
if (possibleReplacements.size() > 1) {
logger.atWarning().log(
- "Fixing ref %s, multiple accounts found with the same email address, while replacing %s",
+ "Fixing ref %s, multiple accounts found with the same email address, while replacing"
+ + " %s",
changeFixProgress.changeMetaRef, accountInfo);
possibleReplacements =
possibleReplacements.entrySet().stream()
diff --git a/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java b/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
index e8c0fda..76871a8 100644
--- a/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
+++ b/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
@@ -159,6 +159,7 @@
HumanComment comment = curMap.get(key);
if (key.equals(uuid)) {
comment.message = newMessage;
+ comment.unresolved = false;
}
comments.add(comment);
}
diff --git a/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java b/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
index 1ead03c..28436db 100644
--- a/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
+++ b/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
@@ -141,7 +141,7 @@
}
private void logInfo(String message) {
- logger.atInfo().log(message);
+ logger.atInfo().log("%s", message);
uiConsumer.accept(message);
}
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index 9345d98..94e11c8 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -377,7 +377,7 @@
bru.setRefLogIdent(refLogIdent != null ? refLogIdent : serverIdent.get());
bru.setAtomic(true);
or.cmds.addTo(bru);
- bru.setAllowNonFastForwards(true);
+ bru.setAllowNonFastForwards(allowNonFastForwards(or.cmds));
for (BatchUpdateListener listener : batchUpdateListeners) {
bru = listener.beforeUpdateRefs(bru);
}
@@ -458,4 +458,27 @@
}
}
}
+
+ /**
+ * Returns true if we should allow non-fast-forwards while performing the batch ref update. Non-ff
+ * updates are necessary in some specific cases:
+ *
+ * <p>1. Draft ref updates are non fast-forward, since the ref always points to a single commit
+ * that has no parents.
+ *
+ * <p>2. NoteDb rewriters.
+ *
+ * <p>3. If any of the receive commands is of type {@link
+ * org.eclipse.jgit.transport.ReceiveCommand.Type#UPDATE_NONFASTFORWARD} (for example due to a
+ * force push).
+ *
+ * <p>Note that we don't need to explicitly allow non fast-forward updates for DELETE commands
+ * since JGit forces the update implicitly in this case.
+ */
+ private boolean allowNonFastForwards(ChainedReceiveCommands receiveCommands) {
+ return !draftUpdates.isEmpty()
+ || !rewriters.isEmpty()
+ || receiveCommands.getCommands().values().stream()
+ .anyMatch(cmd -> cmd.getType().equals(ReceiveCommand.Type.UPDATE_NONFASTFORWARD));
+ }
}
diff --git a/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java b/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
index 7998476..ac9fa48 100644
--- a/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
+++ b/java/com/google/gerrit/server/notedb/RevisionNoteBuilder.java
@@ -21,6 +21,7 @@
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.MultimapBuilder;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.SubmitRequirementResult;
import java.io.ByteArrayOutputStream;
@@ -73,7 +74,13 @@
final Map<Comment.Key, Comment> put;
private final Set<Comment.Key> delete;
- private List<SubmitRequirementResult> submitRequirementResults;
+ /**
+ * Submit requirement results to be stored in the revision note. If this field is null, we don't
+ * store results in the revision note. Otherwise, we store a "submit requirements" section in the
+ * revision note even if it's empty.
+ */
+ @Nullable private List<SubmitRequirementResult> submitRequirementResults;
+
private String pushCert;
private RevisionNoteBuilder(RevisionNote<? extends Comment> base) {
@@ -90,7 +97,6 @@
put = new HashMap<>();
pushCert = null;
}
- submitRequirementResults = new ArrayList<>();
delete = new HashSet<>();
}
@@ -109,7 +115,18 @@
put.put(comment.key, comment);
}
+ /**
+ * Call this method to designate that we should store submit requirement results in the revision
+ * note. Even if no results are added, an empty submit requirements section will be added.
+ */
+ void createEmptySubmitRequirementResults() {
+ submitRequirementResults = new ArrayList<>();
+ }
+
void putSubmitRequirementResult(SubmitRequirementResult result) {
+ if (submitRequirementResults == null) {
+ submitRequirementResults = new ArrayList<>();
+ }
submitRequirementResults.add(result);
}
@@ -140,19 +157,19 @@
private void buildNoteJson(ChangeNoteJson noteUtil, OutputStream out) throws IOException {
ListMultimap<Integer, Comment> comments = buildCommentMap();
- if (submitRequirementResults.isEmpty() && comments.isEmpty() && pushCert == null) {
+ if (submitRequirementResults == null && comments.isEmpty() && pushCert == null) {
return;
}
RevisionNoteData data = new RevisionNoteData();
data.comments = COMMENT_ORDER.sortedCopy(comments.values());
data.pushCert = pushCert;
- if (!submitRequirementResults.isEmpty()) {
- data.submitRequirementResults =
- submitRequirementResults.stream()
- .sorted(SUBMIT_REQUIREMENT_RESULT_COMPARATOR)
- .collect(Collectors.toList());
- }
+ data.submitRequirementResults =
+ submitRequirementResults == null
+ ? null
+ : submitRequirementResults.stream()
+ .sorted(SUBMIT_REQUIREMENT_RESULT_COMPARATOR)
+ .collect(Collectors.toList());
try (OutputStreamWriter osw = new OutputStreamWriter(out, UTF_8)) {
noteUtil.getGson().toJson(data, osw);
diff --git a/java/com/google/gerrit/server/notedb/RevisionNoteMap.java b/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
index 5a0b67b..98c9873 100644
--- a/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
+++ b/java/com/google/gerrit/server/notedb/RevisionNoteMap.java
@@ -16,7 +16,6 @@
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.entities.Comment;
-import com.google.gerrit.entities.HumanComment;
import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
@@ -42,7 +41,7 @@
}
static RevisionNoteMap<ChangeRevisionNote> parse(
- ChangeNoteJson noteJson, ObjectReader reader, NoteMap noteMap, HumanComment.Status status)
+ ChangeNoteJson noteJson, ObjectReader reader, NoteMap noteMap, Comment.Status status)
throws ConfigInvalidException, IOException {
ImmutableMap.Builder<ObjectId, ChangeRevisionNote> result = ImmutableMap.builder();
for (Note note : noteMap) {
diff --git a/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java b/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
index 895f378..edf5bd3 100644
--- a/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
+++ b/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
@@ -28,9 +28,9 @@
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -57,14 +57,14 @@
@Assisted("effective") Account.Id accountId,
@Assisted("real") Account.Id realAccountId,
PersonIdent authorIdent,
- Date when);
+ Instant when);
RobotCommentUpdate create(
Change change,
@Assisted("effective") Account.Id accountId,
@Assisted("real") Account.Id realAccountId,
PersonIdent authorIdent,
- Date when);
+ Instant when);
}
private List<RobotComment> put = new ArrayList<>();
@@ -77,7 +77,7 @@
@Assisted("effective") Account.Id accountId,
@Assisted("real") Account.Id realAccountId,
@Assisted PersonIdent authorIdent,
- @Assisted Date when) {
+ @Assisted Instant when) {
super(noteUtil, serverIdent, notes, null, accountId, realAccountId, authorIdent, when);
}
@@ -89,7 +89,7 @@
@Assisted("effective") Account.Id accountId,
@Assisted("real") Account.Id realAccountId,
@Assisted PersonIdent authorIdent,
- @Assisted Date when) {
+ @Assisted Instant when) {
super(noteUtil, serverIdent, null, change, accountId, realAccountId, authorIdent, when);
}
diff --git a/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java b/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java
index d128633..6d5f4be 100644
--- a/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java
+++ b/java/com/google/gerrit/server/notedb/StoreSubmitRequirementsOp.java
@@ -14,31 +14,31 @@
package com.google.gerrit.server.notedb;
+import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.server.experiments.ExperimentFeatures;
import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
-import com.google.gerrit.server.project.SubmitRequirementsEvaluator;
-import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
/** A {@link BatchUpdateOp} that stores the evaluated submit requirements of a change in NoteDb. */
public class StoreSubmitRequirementsOp implements BatchUpdateOp {
- private final ChangeData.Factory changeDataFactory;
- private final SubmitRequirementsEvaluator evaluator;
private final boolean storeRequirementsInNoteDb;
+ private final Collection<SubmitRequirementResult> submitRequirementResults;
public interface Factory {
- StoreSubmitRequirementsOp create();
+ StoreSubmitRequirementsOp create(Collection<SubmitRequirementResult> submitRequirements);
}
@Inject
public StoreSubmitRequirementsOp(
- ChangeData.Factory changeDataFactory,
ExperimentFeatures experimentFeatures,
- SubmitRequirementsEvaluator evaluator) {
- this.changeDataFactory = changeDataFactory;
- this.evaluator = evaluator;
+ @Assisted Collection<SubmitRequirementResult> submitRequirementResults) {
+ this.submitRequirementResults = submitRequirementResults;
this.storeRequirementsInNoteDb =
experimentFeatures.isFeatureEnabled(
ExperimentFeaturesConstants
@@ -51,21 +51,16 @@
// Temporarily stop storing submit requirements in NoteDb when the change is merged.
return false;
}
- // Create ChangeData using the project/change IDs instead of ctx.getChange(). We do that because
- // for changes requiring a rebase before submission (e.g. if submit type = RebaseAlways), the
- // RebaseOp inserts a new patchset that is visible here (via Change#getCurrentPatchset). If we
- // then try to get ChangeData#currentPatchset it will return null, since it loads patchsets from
- // NoteDb but tries to find the patchset with the ID of the one just inserted by the rebase op.
- // Note that this implementation means that, in this case, submit requirement results will be
- // stored in change notes of the pre last patchset commit. This is fine since submit requirement
- // results should evaluate to the exact same results for both commits. Additionally, the
- // pre-last commit is the one for which we displayed the submit requirement results of the last
- // patchset to the user before it was merged.
- ChangeData changeData = changeDataFactory.create(ctx.getProject(), ctx.getChange().getId());
ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
- // We do not want to store submit requirements in NoteDb for legacy submit records
- update.putSubmitRequirementResults(
- evaluator.evaluateAllRequirements(changeData, /* includeLegacy= */ false).values());
- return !changeData.submitRequirements().isEmpty();
+ List<SubmitRequirementResult> nonLegacySubmitRequirements =
+ submitRequirementResults.stream()
+ // We don't store results for legacy submit requirements in NoteDb. While
+ // surfacing submit requirements for closed changes, we load submit records
+ // from NoteDb and convert them to submit requirement results. See
+ // ChangeData#submitRequirements().
+ .filter(srResult -> !srResult.isLegacy())
+ .collect(Collectors.toList());
+ update.putSubmitRequirementResults(nonLegacySubmitRequirements);
+ return !submitRequirementResults.isEmpty();
}
}
diff --git a/java/com/google/gerrit/server/notedb/SubmitRequirementProtoConverter.java b/java/com/google/gerrit/server/notedb/SubmitRequirementProtoConverter.java
index 3caa4d4..dac71ea 100644
--- a/java/com/google/gerrit/server/notedb/SubmitRequirementProtoConverter.java
+++ b/java/com/google/gerrit/server/notedb/SubmitRequirementProtoConverter.java
@@ -36,6 +36,8 @@
SubmitRequirementResultProto.getDescriptor().findFieldByNumber(4);
private static final FieldDescriptor SR_LEGACY_FIELD =
SubmitRequirementResultProto.getDescriptor().findFieldByNumber(6);
+ private static final FieldDescriptor SR_FORCED_FIELD =
+ SubmitRequirementResultProto.getDescriptor().findFieldByNumber(7);
@Override
public SubmitRequirementResultProto toProto(SubmitRequirementResult r) {
@@ -46,6 +48,9 @@
if (r.legacy().isPresent()) {
builder.setLegacy(r.legacy().get());
}
+ if (r.forced().isPresent()) {
+ builder.setForced(r.forced().get());
+ }
if (r.applicabilityExpressionResult().isPresent()) {
builder.setApplicabilityExpressionResult(
SubmitRequirementExpressionResultSerializer.serialize(
@@ -71,6 +76,9 @@
if (proto.hasField(SR_LEGACY_FIELD)) {
builder.legacy(Optional.of(proto.getLegacy()));
}
+ if (proto.hasField(SR_FORCED_FIELD)) {
+ builder.forced(Optional.of(proto.getForced()));
+ }
if (proto.hasField(SR_APPLICABILITY_EXPR_RESULT_FIELD)) {
builder.applicabilityExpressionResult(
Optional.of(
diff --git a/java/com/google/gerrit/server/patch/AutoMerger.java b/java/com/google/gerrit/server/patch/AutoMerger.java
index 2529c04..fa27657 100644
--- a/java/com/google/gerrit/server/patch/AutoMerger.java
+++ b/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -145,7 +145,7 @@
return existingCommit.get();
}
counter.increment(OperationType.IN_MEMORY_WRITE);
- logger.atInfo().log("Computing in-memory AutoMerge for " + merge.name());
+ logger.atInfo().log("Computing in-memory AutoMerge for %s", merge.name());
try (Timer1.Context<OperationType> ignored = latency.start(OperationType.IN_MEMORY_WRITE)) {
return rw.parseCommit(createAutoMergeCommit(repo.getConfig(), rw, ins, merge, mergeStrategy));
}
diff --git a/java/com/google/gerrit/server/patch/DiffOperations.java b/java/com/google/gerrit/server/patch/DiffOperations.java
index d2da736..a265337 100644
--- a/java/com/google/gerrit/server/patch/DiffOperations.java
+++ b/java/com/google/gerrit/server/patch/DiffOperations.java
@@ -18,10 +18,14 @@
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.Patch.ChangeType;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.server.patch.filediff.FileDiffOutput;
+import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
import java.util.Map;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevWalk;
/**
* An interface for all file diff related operations. Clients should use this interface to request:
@@ -56,7 +60,31 @@
* an internal error occurred in Git while evaluating the diff.
*/
Map<String, FileDiffOutput> listModifiedFilesAgainstParent(
- Project.NameKey project, ObjectId newCommit, int parentNum) throws DiffNotAvailableException;
+ Project.NameKey project, ObjectId newCommit, int parentNum, DiffOptions diffOptions)
+ throws DiffNotAvailableException;
+
+ /**
+ * This method is similar to {@link #listModifiedFilesAgainstParent(NameKey, ObjectId, int,
+ * DiffOptions)} but loads the modified files directly instead of retrieving them from the diff
+ * cache.
+ *
+ * <p>A RevWalk and repoConfig are also supplied and are used to look up the commit IDs. This is
+ * useful in case one the commits is currently being created, that's why the {@code revWalk}
+ * parameter is needed.
+ *
+ * <p>Note that rename detection is disabled for this method.
+ *
+ * @return a map of file paths to {@link ModifiedFile}. The {@link ModifiedFile} contains the
+ * old/new file paths and the change type (added, deleted, etc...).
+ */
+ Map<String, ModifiedFile> loadModifiedFilesAgainstParent(
+ Project.NameKey project,
+ ObjectId newCommit,
+ int parentNum,
+ DiffOptions diffOptions,
+ RevWalk revWalk,
+ Config repoConfig)
+ throws DiffNotAvailableException;
/**
* Returns the list of added, deleted or modified files between two commits (patchsets). The
@@ -72,7 +100,30 @@
* diff.
*/
Map<String, FileDiffOutput> listModifiedFiles(
- Project.NameKey project, ObjectId oldCommit, ObjectId newCommit)
+ Project.NameKey project, ObjectId oldCommit, ObjectId newCommit, DiffOptions diffOptions)
+ throws DiffNotAvailableException;
+
+ /**
+ * This method is similar to {@link #listModifiedFilesAgainstParent(NameKey, ObjectId, int,
+ * DiffOptions)} but loads the modified files directly instead of retrieving them from the diff
+ * cache.
+ *
+ * <p>A RevWalk and repoConfig are also supplied and are used to look up the commit IDs. This is
+ * useful in case one the commits is currently being created, that's why the {@code revWalk}
+ * parameter is needed.
+ *
+ * <p>Note that rename detection is disabled for this method.
+ *
+ * @return a map of file paths to {@link ModifiedFile}. The {@link ModifiedFile} contains the
+ * old/new file paths and the change type (added, deleted, etc...).
+ */
+ Map<String, ModifiedFile> loadModifiedFiles(
+ Project.NameKey project,
+ ObjectId oldCommit,
+ ObjectId newCommit,
+ DiffOptions diffOptions,
+ RevWalk revWalk,
+ Config repoConfig)
throws DiffNotAvailableException;
/**
diff --git a/java/com/google/gerrit/server/patch/DiffOperationsImpl.java b/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
index 3423b32..a5b4d0a 100644
--- a/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
+++ b/java/com/google/gerrit/server/patch/DiffOperationsImpl.java
@@ -47,7 +47,16 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.io.DisabledOutputStream;
/**
* Provides different file diff operations. Uses the underlying Git/Gerrit caches to speed up the
@@ -57,6 +66,19 @@
public class DiffOperationsImpl implements DiffOperations {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final ImmutableMap<DiffEntry.ChangeType, Patch.ChangeType> changeTypeMap =
+ ImmutableMap.of(
+ DiffEntry.ChangeType.ADD,
+ Patch.ChangeType.ADDED,
+ DiffEntry.ChangeType.MODIFY,
+ Patch.ChangeType.MODIFIED,
+ DiffEntry.ChangeType.DELETE,
+ Patch.ChangeType.DELETED,
+ DiffEntry.ChangeType.RENAME,
+ Patch.ChangeType.RENAMED,
+ DiffEntry.ChangeType.COPY,
+ Patch.ChangeType.COPIED);
+
private static final int RENAME_SCORE = 60;
private static final DiffAlgorithm DEFAULT_DIFF_ALGORITHM =
DiffAlgorithm.HISTOGRAM_WITH_FALLBACK_MYERS;
@@ -91,10 +113,11 @@
@Override
public Map<String, FileDiffOutput> listModifiedFilesAgainstParent(
- Project.NameKey project, ObjectId newCommit, int parent) throws DiffNotAvailableException {
+ Project.NameKey project, ObjectId newCommit, int parent, DiffOptions diffOptions)
+ throws DiffNotAvailableException {
try {
DiffParameters diffParams = computeDiffParameters(project, newCommit, parent);
- return getModifiedFiles(diffParams);
+ return getModifiedFiles(diffParams, diffOptions);
} catch (IOException e) {
throw new DiffNotAvailableException(
"Failed to evaluate the parent/base commit for commit " + newCommit, e);
@@ -102,8 +125,29 @@
}
@Override
+ public Map<String, ModifiedFile> loadModifiedFilesAgainstParent(
+ Project.NameKey project,
+ ObjectId newCommit,
+ int parentNum,
+ DiffOptions diffOptions,
+ RevWalk revWalk,
+ Config repoConfig)
+ throws DiffNotAvailableException {
+ try {
+ DiffParameters diffParams = computeDiffParameters(project, newCommit, parentNum);
+ return loadModifiedFilesWithoutCache(project, diffParams, revWalk, repoConfig);
+ } catch (IOException e) {
+ throw new DiffNotAvailableException(
+ String.format(
+ "Failed to evaluate the parent/base commit for commit '%s' with parentNum=%d",
+ newCommit, parentNum),
+ e);
+ }
+ }
+
+ @Override
public Map<String, FileDiffOutput> listModifiedFiles(
- Project.NameKey project, ObjectId oldCommit, ObjectId newCommit)
+ Project.NameKey project, ObjectId oldCommit, ObjectId newCommit, DiffOptions diffOptions)
throws DiffNotAvailableException {
DiffParameters params =
DiffParameters.builder()
@@ -112,7 +156,26 @@
.baseCommit(oldCommit)
.comparisonType(ComparisonType.againstOtherPatchSet())
.build();
- return getModifiedFiles(params);
+ return getModifiedFiles(params, diffOptions);
+ }
+
+ @Override
+ public Map<String, ModifiedFile> loadModifiedFiles(
+ Project.NameKey project,
+ ObjectId oldCommit,
+ ObjectId newCommit,
+ DiffOptions diffOptions,
+ RevWalk revWalk,
+ Config repoConfig)
+ throws DiffNotAvailableException {
+ DiffParameters params =
+ DiffParameters.builder()
+ .project(project)
+ .newCommit(newCommit)
+ .baseCommit(oldCommit)
+ .comparisonType(ComparisonType.againstOtherPatchSet())
+ .build();
+ return loadModifiedFilesWithoutCache(project, params, revWalk, repoConfig);
}
@Override
@@ -161,8 +224,8 @@
return getModifiedFileForKey(key);
}
- private ImmutableMap<String, FileDiffOutput> getModifiedFiles(DiffParameters diffParams)
- throws DiffNotAvailableException {
+ private ImmutableMap<String, FileDiffOutput> getModifiedFiles(
+ DiffParameters diffParams, DiffOptions diffOptions) throws DiffNotAvailableException {
try {
Project.NameKey project = diffParams.project();
ObjectId newCommit = diffParams.newCommit();
@@ -211,7 +274,7 @@
/* whitespace= */ null))
.forEach(fileCacheKeys::add);
}
- return getModifiedFilesForKeys(fileCacheKeys);
+ return getModifiedFilesForKeys(fileCacheKeys, diffOptions);
} catch (IOException e) {
throw new DiffNotAvailableException(e);
}
@@ -219,7 +282,8 @@
private FileDiffOutput getModifiedFileForKey(FileDiffCacheKey key)
throws DiffNotAvailableException {
- Map<String, FileDiffOutput> diffList = getModifiedFilesForKeys(ImmutableList.of(key));
+ Map<String, FileDiffOutput> diffList =
+ getModifiedFilesForKeys(ImmutableList.of(key), DiffOptions.DEFAULTS);
return diffList.containsKey(key.newFilePath())
? diffList.get(key.newFilePath())
: FileDiffOutput.empty(key.newFilePath(), key.oldCommit(), key.newCommit());
@@ -230,8 +294,8 @@
* results, e.g. due to timeouts in the cache loader, this method requests the diff again using
* the fallback algorithm {@link DiffAlgorithm#HISTOGRAM_NO_FALLBACK}.
*/
- private ImmutableMap<String, FileDiffOutput> getModifiedFilesForKeys(List<FileDiffCacheKey> keys)
- throws DiffNotAvailableException {
+ private ImmutableMap<String, FileDiffOutput> getModifiedFilesForKeys(
+ List<FileDiffCacheKey> keys, DiffOptions diffOptions) throws DiffNotAvailableException {
ImmutableMap<FileDiffCacheKey, FileDiffOutput> fileDiffs = fileDiffCache.getAll(keys);
List<FileDiffCacheKey> fallbackKeys = new ArrayList<>();
@@ -260,7 +324,7 @@
}
}
result.addAll(fileDiffCache.getAll(fallbackKeys).values());
- return mapByFilePath(result.build());
+ return mapByFilePath(result.build(), diffOptions);
}
/**
@@ -268,11 +332,12 @@
* represent the old file path for deleted files, or the new path otherwise.
*/
private ImmutableMap<String, FileDiffOutput> mapByFilePath(
- ImmutableCollection<FileDiffOutput> fileDiffOutputs) {
+ ImmutableCollection<FileDiffOutput> fileDiffOutputs, DiffOptions diffOptions) {
ImmutableMap.Builder<String, FileDiffOutput> diffs = ImmutableMap.builder();
for (FileDiffOutput fileDiffOutput : fileDiffOutputs) {
- if (fileDiffOutput.isEmpty() || allDueToRebase(fileDiffOutput)) {
+ if (fileDiffOutput.isEmpty()
+ || (diffOptions.skipFilesWithAllEditsDueToRebase() && allDueToRebase(fileDiffOutput))) {
continue;
}
if (fileDiffOutput.changeType() == ChangeType.DELETED) {
@@ -286,8 +351,8 @@
private static boolean allDueToRebase(FileDiffOutput fileDiffOutput) {
return fileDiffOutput.allEditsDueToRebase()
- && (!(fileDiffOutput.changeType() == ChangeType.RENAMED
- || fileDiffOutput.changeType() == ChangeType.COPIED));
+ && !(fileDiffOutput.changeType() == ChangeType.RENAMED
+ || fileDiffOutput.changeType() == ChangeType.COPIED);
}
private boolean isMergeAgainstParent(ComparisonType cmp, Project.NameKey project, ObjectId commit)
@@ -326,6 +391,53 @@
.build();
}
+ /** Loads the modified file paths between two commits without inspecting the diff cache. */
+ private static Map<String, ModifiedFile> loadModifiedFilesWithoutCache(
+ Project.NameKey project, DiffParameters diffParams, RevWalk revWalk, Config repoConfig)
+ throws DiffNotAvailableException {
+ ObjectId newCommit = diffParams.newCommit();
+ ObjectId oldCommit = diffParams.baseCommit();
+ try {
+ ObjectReader reader = revWalk.getObjectReader();
+ List<DiffEntry> diffEntries;
+ try (DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
+ df.setReader(reader, repoConfig);
+ df.setDetectRenames(false);
+ diffEntries = df.scan(oldCommit.equals(ObjectId.zeroId()) ? null : oldCommit, newCommit);
+ }
+ List<ModifiedFile> modifiedFiles =
+ diffEntries.stream()
+ .map(
+ entry ->
+ ModifiedFile.builder()
+ .changeType(toChangeType(entry.getChangeType()))
+ .oldPath(getGitPath(entry.getOldPath()))
+ .newPath(getGitPath(entry.getNewPath()))
+ .build())
+ .collect(Collectors.toList());
+ return DiffUtil.mergeRewrittenModifiedFiles(modifiedFiles).stream()
+ .collect(ImmutableMap.toImmutableMap(ModifiedFile::getDefaultPath, Function.identity()));
+ } catch (IOException e) {
+ throw new DiffNotAvailableException(
+ String.format(
+ "Failed to compute the modified files for project '%s',"
+ + " old commit '%s', new commit '%s'.",
+ project, oldCommit.name(), newCommit.name()),
+ e);
+ }
+ }
+
+ private static Optional<String> getGitPath(String path) {
+ return path.equals(DiffEntry.DEV_NULL) ? Optional.empty() : Optional.of(path);
+ }
+
+ private static Patch.ChangeType toChangeType(DiffEntry.ChangeType changeType) {
+ if (!changeTypeMap.containsKey(changeType)) {
+ throw new IllegalArgumentException("Unsupported type " + changeType);
+ }
+ return changeTypeMap.get(changeType);
+ }
+
@AutoValue
abstract static class DiffParameters {
abstract Project.NameKey project();
diff --git a/java/com/google/gerrit/server/patch/DiffOptions.java b/java/com/google/gerrit/server/patch/DiffOptions.java
new file mode 100644
index 0000000..4d54be1
--- /dev/null
+++ b/java/com/google/gerrit/server/patch/DiffOptions.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.patch;
+
+import com.google.auto.value.AutoValue;
+
+@AutoValue
+public abstract class DiffOptions {
+ public static final DiffOptions DEFAULTS =
+ DiffOptions.builder().skipFilesWithAllEditsDueToRebase(true).build();
+
+ public abstract boolean skipFilesWithAllEditsDueToRebase();
+
+ public static DiffOptions.Builder builder() {
+ return new AutoValue_DiffOptions.Builder();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder skipFilesWithAllEditsDueToRebase(boolean value);
+
+ public abstract DiffOptions build();
+ }
+}
diff --git a/java/com/google/gerrit/server/patch/DiffSummaryLoader.java b/java/com/google/gerrit/server/patch/DiffSummaryLoader.java
index fcce672..246544b 100644
--- a/java/com/google/gerrit/server/patch/DiffSummaryLoader.java
+++ b/java/com/google/gerrit/server/patch/DiffSummaryLoader.java
@@ -48,8 +48,9 @@
ObjectId newId = key.toPatchListKey().getNewId();
Map<String, FileDiffOutput> diffList =
oldId == null
- ? diffOperations.listModifiedFilesAgainstParent(project, newId, /* parentNum= */ 0)
- : diffOperations.listModifiedFiles(project, oldId, newId);
+ ? diffOperations.listModifiedFilesAgainstParent(
+ project, newId, /* parentNum= */ 0, DiffOptions.DEFAULTS)
+ : diffOperations.listModifiedFiles(project, oldId, newId, DiffOptions.DEFAULTS);
return toDiffSummary(diffList);
}
diff --git a/java/com/google/gerrit/server/patch/DiffUtil.java b/java/com/google/gerrit/server/patch/DiffUtil.java
index 1e88f9f..e75d50c 100644
--- a/java/com/google/gerrit/server/patch/DiffUtil.java
+++ b/java/com/google/gerrit/server/patch/DiffUtil.java
@@ -15,9 +15,16 @@
package com.google.gerrit.server.patch;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.gerrit.entities.Patch.ChangeType;
import com.google.gerrit.server.patch.diff.ModifiedFilesCache;
import com.google.gerrit.server.patch.gitdiff.GitModifiedFilesCache;
+import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -29,6 +36,40 @@
public class DiffUtil {
/**
+ * Return the {@code modifiedFiles} input list while merging rewritten entries.
+ *
+ * <p>Background: In some cases, JGit returns two diff entries (ADDED/DELETED, RENAMED/DELETED,
+ * etc...) for the same file path. This happens e.g. when a file's mode is changed between
+ * patchsets, for example converting a symlink file to a regular file. We identify this case and
+ * return a single modified file with changeType = {@link ChangeType#REWRITE}.
+ */
+ public static List<ModifiedFile> mergeRewrittenModifiedFiles(List<ModifiedFile> modifiedFiles) {
+ List<ModifiedFile> result = new ArrayList<>();
+ ListMultimap<String, ModifiedFile> byPath = ArrayListMultimap.create();
+ modifiedFiles.stream()
+ .forEach(
+ f -> {
+ if (f.changeType() == ChangeType.DELETED) {
+ byPath.get(f.oldPath().get()).add(f);
+ } else {
+ byPath.get(f.newPath().get()).add(f);
+ }
+ });
+ for (String path : byPath.keySet()) {
+ List<ModifiedFile> entries = byPath.get(path);
+ if (entries.size() == 1) {
+ result.add(entries.get(0));
+ } else {
+ // More than one. Return a single REWRITE entry.
+ // Convert the first entry (prioritized according to change type enum order) to REWRITE
+ entries.sort(Comparator.comparingInt(o -> o.changeType().ordinal()));
+ result.add(entries.get(0).toBuilder().changeType(ChangeType.REWRITE).build());
+ }
+ }
+ return result;
+ }
+
+ /**
* Returns the Git tree object ID pointed to by the commitId parameter.
*
* @param rw a {@link RevWalk} of an opened repository that is used to walk the commit graph.
diff --git a/java/com/google/gerrit/server/patch/MergeListBuilder.java b/java/com/google/gerrit/server/patch/MergeListBuilder.java
index 433fcad..337d940 100644
--- a/java/com/google/gerrit/server/patch/MergeListBuilder.java
+++ b/java/com/google/gerrit/server/patch/MergeListBuilder.java
@@ -22,7 +22,7 @@
import org.eclipse.jgit.revwalk.RevWalk;
public class MergeListBuilder {
- public static List<RevCommit> build(RevWalk rw, RevCommit merge, int uninterestingParent)
+ public static ImmutableList<RevCommit> build(RevWalk rw, RevCommit merge, int uninterestingParent)
throws IOException {
rw.reset();
rw.parseBody(merge);
@@ -45,6 +45,6 @@
while ((c = rw.next()) != null) {
result.add(c);
}
- return result;
+ return ImmutableList.copyOf(result);
}
}
diff --git a/java/com/google/gerrit/server/patch/PatchFile.java b/java/com/google/gerrit/server/patch/PatchFile.java
index 81355cc..7a8180bd 100644
--- a/java/com/google/gerrit/server/patch/PatchFile.java
+++ b/java/com/google/gerrit/server/patch/PatchFile.java
@@ -57,8 +57,9 @@
throws IOException {
this.repo = repo;
this.diff =
- modifiedFiles.values().stream()
- .filter(f -> f.newPath().isPresent() && f.newPath().get().equals(fileName))
+ modifiedFiles.entrySet().stream()
+ .filter(f -> f.getKey().equals(fileName))
+ .map(Map.Entry::getValue)
.findFirst()
.orElse(FileDiffOutput.empty(fileName, ObjectId.zeroId(), ObjectId.zeroId()));
@@ -96,7 +97,13 @@
bTree = null;
} else {
if (diff.oldCommitId() != null) {
- aTree = rw.parseTree(diff.oldCommitId());
+ if (diff.oldCommitId().equals(ObjectId.zeroId())) {
+ // DiffOperations returns ObjectId.zeroId if newCommit is a root commit, i.e. has no
+ // parents.
+ aTree = null;
+ } else {
+ aTree = rw.parseTree(diff.oldCommitId());
+ }
} else {
final RevCommit p = bCommit.getParent(0);
rw.parseHeaders(p);
diff --git a/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java b/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
index 572d73d..2385a70 100644
--- a/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
+++ b/java/com/google/gerrit/server/patch/SubmitWithStickyApprovalDiff.java
@@ -70,6 +70,7 @@
*/
public class SubmitWithStickyApprovalDiff {
private static final int HEAP_EST_SIZE = 32 * 1024;
+ private static final int DEFAULT_POST_SUBMIT_SIZE_LIMIT = 300 * 1024; // 300 KiB
private final DiffOperations diffOperations;
private final ProjectCache projectCache;
@@ -88,6 +89,15 @@
this.projectCache = projectCache;
this.patchScriptFactoryFactory = patchScriptFactoryFactory;
this.repositoryManager = repositoryManager;
+ // (November 2021) We define the max cumulative comment size to 300 KIB since it's a reasonable
+ // size that is large enough for all purposes but not too large to choke the change index by
+ // exceeding the cumulative comment size limit (new comments are not allowed once the limit
+ // is reached). At Google, the change index limit is 5MB, while the cumulative size limit is
+ // set at 3MB. In this example, we can reach at most 3.3MB hence we ensure not to exceed the
+ // limit of 5MB.
+ // The reason we exclude the post submit diff from the cumulative comment size limit is
+ // just because change messages not currently being validated. Change messages are still
+ // counted towards the limit, though.
maxCumulativeSize =
serverConfig.getInt(
"change",
@@ -129,7 +139,9 @@
diff.append("The change was submitted with unreviewed changes in the following files:\n\n");
TemporaryBuffer.Heap buffer =
- new TemporaryBuffer.Heap(Math.min(HEAP_EST_SIZE, maxCumulativeSize), maxCumulativeSize);
+ new TemporaryBuffer.Heap(
+ Math.min(HEAP_EST_SIZE, DEFAULT_POST_SUBMIT_SIZE_LIMIT),
+ DEFAULT_POST_SUBMIT_SIZE_LIMIT);
try (Repository repository = repositoryManager.openRepository(notes.getProjectName());
DiffFormatter formatter = new DiffFormatter(buffer)) {
formatter.setRepository(repository);
@@ -150,6 +162,12 @@
throw e;
}
}
+ if (formatterResult != null) {
+ int addedBytes = formatterResult.stream().mapToInt(String::length).sum();
+ if (!CommentCumulativeSizeValidator.isEnoughSpace(notes, addedBytes, maxCumulativeSize)) {
+ isDiffTooLarge = true;
+ }
+ }
for (FileDiffOutput fileDiff : modifiedFilesList) {
diff.append(
getDiffForFile(
@@ -299,7 +317,8 @@
private Map<String, FileDiffOutput> listModifiedFiles(
Project.NameKey project, PatchSet ps, PatchSet priorPatchSet) {
try {
- return diffOperations.listModifiedFiles(project, priorPatchSet.commitId(), ps.commitId());
+ return diffOperations.listModifiedFiles(
+ project, priorPatchSet.commitId(), ps.commitId(), DiffOptions.DEFAULTS);
} catch (DiffNotAvailableException ex) {
throw new StorageException(
"failed to compute difference in files, so won't post diff messsage on submit although "
diff --git a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
index 460c2e2..9c4d601 100644
--- a/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/diff/ModifiedFilesCacheImpl.java
@@ -18,14 +18,11 @@
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
-import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ListMultimap;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.entities.Patch.ChangeType;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.DiffNotAvailableException;
@@ -40,7 +37,6 @@
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import java.io.IOException;
-import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
@@ -143,7 +139,7 @@
.bTree(bTree)
.renameScore(key.renameScore())
.build();
- List<ModifiedFile> modifiedFiles = mergeRewrittenEntries(gitCache.get(gitKey));
+ List<ModifiedFile> modifiedFiles = DiffUtil.mergeRewrittenModifiedFiles(gitCache.get(gitKey));
if (key.aCommit().equals(ObjectId.zeroId())) {
return ImmutableList.copyOf(modifiedFiles);
}
@@ -206,37 +202,5 @@
// value as the set of file paths shouldn't contain it.
return touchedFilePaths.contains(oldFilePath) || touchedFilePaths.contains(newFilePath);
}
-
- /**
- * Return the {@code modifiedFiles} input list while merging rewritten entries.
- *
- * <p>Background: In some cases, JGit returns two diff entries (ADDED/DELETED, RENAMED/DELETED,
- * etc...) for the same file path. This happens e.g. when a file's mode is changed between
- * patchsets, for example converting a symlink file to a regular file. We identify this case and
- * return a single modified file with changeType = {@link ChangeType#REWRITE}.
- */
- private static List<ModifiedFile> mergeRewrittenEntries(List<ModifiedFile> modifiedFiles) {
- List<ModifiedFile> result = new ArrayList<>();
- ListMultimap<String, ModifiedFile> byPath = ArrayListMultimap.create();
- modifiedFiles.stream()
- .forEach(
- f -> {
- if (f.changeType() == ChangeType.DELETED) {
- byPath.get(f.oldPath().get()).add(f);
- } else {
- byPath.get(f.newPath().get()).add(f);
- }
- });
- for (String path : byPath.keySet()) {
- List<ModifiedFile> entries = byPath.get(path);
- if (entries.size() == 1) {
- result.add(entries.get(0));
- } else {
- // More than one. Return a single REWRITE entry.
- result.add(entries.get(0).toBuilder().changeType(ChangeType.REWRITE).build());
- }
- }
- return result;
- }
}
}
diff --git a/java/com/google/gerrit/server/patch/gitdiff/ModifiedFile.java b/java/com/google/gerrit/server/patch/gitdiff/ModifiedFile.java
index f4e7ca3..3596a54 100644
--- a/java/com/google/gerrit/server/patch/gitdiff/ModifiedFile.java
+++ b/java/com/google/gerrit/server/patch/gitdiff/ModifiedFile.java
@@ -47,6 +47,10 @@
*/
public abstract Optional<String> newPath();
+ public String getDefaultPath() {
+ return newPath().isPresent() ? newPath().get() : oldPath().get();
+ }
+
public static Builder builder() {
return new AutoValue_ModifiedFile.Builder();
}
diff --git a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
index 6a59872..0888f3f 100644
--- a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
@@ -59,7 +59,6 @@
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
-import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
@@ -156,16 +155,6 @@
}
static class Loader extends CacheLoader<GitFileDiffCacheKey, GitFileDiff> {
- /**
- * Extractor for the file path from a {@link DiffEntry}. Returns the old file path if the entry
- * corresponds to a deleted file, otherwise it returns the new file path.
- */
- private static final Function<DiffEntry, String> pathExtractor =
- (DiffEntry entry) ->
- entry.getChangeType().equals(ChangeType.DELETE)
- ? entry.getOldPath()
- : entry.getNewPath();
-
private final GitRepositoryManager repoManager;
private final ExecutorService diffExecutor;
private final long timeoutMillis;
@@ -291,10 +280,10 @@
diffOptions.newTree());
return diffEntries.stream()
- .filter(d -> filePathsSet.contains(pathExtractor.apply(d)))
+ .filter(d -> filePathsSet.contains(extractPath(d)))
.collect(
Multimaps.toMultimap(
- d -> pathExtractor.apply(d),
+ Loader::extractPath,
identity(),
MultimapBuilder.treeKeys().arrayListValues()::build));
}
@@ -378,6 +367,16 @@
throw new IOException(e.getMessage(), e.getCause());
}
}
+
+ /**
+ * Extract the file path from a {@link DiffEntry}. Returns the old file path if the entry
+ * corresponds to a deleted file, otherwise it returns the new file path.
+ */
+ private static String extractPath(DiffEntry diffEntry) {
+ return diffEntry.getChangeType().equals(ChangeType.DELETE)
+ ? diffEntry.getOldPath()
+ : diffEntry.getNewPath();
+ }
}
/**
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index 37c773a..8d432c8 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -92,8 +92,7 @@
/** Can this user revert this change? */
private boolean canRevert() {
- return (refControl.canRevert())
- && refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE);
+ return refControl.canRevert() && refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE);
}
/** The range of permitted values associated with a label permission. */
@@ -257,7 +256,7 @@
case ABANDON:
return canAbandon();
case DELETE:
- return (getProjectControl().isAdmin() || (refControl.canDeleteChanges(isOwner())));
+ return getProjectControl().isAdmin() || refControl.canDeleteChanges(isOwner());
case ADD_PATCH_SET:
return canAddPatchSet();
case EDIT_ASSIGNEE:
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index 1b528d7..e523d76 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -112,7 +112,7 @@
}
/** Filters given refs and tags by visibility. */
- Collection<Ref> filter(Collection<Ref> refs, Repository repo, RefFilterOptions opts)
+ ImmutableList<Ref> filter(Collection<Ref> refs, Repository repo, RefFilterOptions opts)
throws PermissionBackendException {
visibleChangesCache = visibleChangesCacheFactory.create(projectControl, repo);
logger.atFinest().log(
@@ -138,7 +138,7 @@
boolean isChangeRefVisisble = canSeeSingleChangeRef(repo, refName);
if (isChangeRefVisisble) {
logger.atFinest().log("Change ref %s is visible", refName);
- return refs;
+ return ImmutableList.copyOf(refs);
}
logger.atFinest().log("Filter out non-visible change ref %s", refName);
return ImmutableList.of();
@@ -149,7 +149,7 @@
// we have to investigate separately (deferred tags) then perform a reachability check starting
// from all visible branches (refs/heads/*).
Result initialRefFilter = filterRefs(new ArrayList<>(refs), opts);
- List<Ref> visibleRefs = initialRefFilter.visibleRefs();
+ List<Ref> visibleRefs = new ArrayList<>(initialRefFilter.visibleRefs());
if (!initialRefFilter.deferredTags().isEmpty()) {
try (TraceTimer traceTimer = TraceContext.newTimer("Check visibility of deferred tags")) {
Result allVisibleBranches = filterRefs(getTaggableRefs(repo), opts);
@@ -178,7 +178,7 @@
}
logger.atFinest().log("visible refs = %s", visibleRefs);
- return visibleRefs;
+ return ImmutableList.copyOf(visibleRefs);
}
/**
@@ -198,13 +198,15 @@
skipFilterCount.increment();
logger.atFinest().log(
"Fast path, all refs are visible because user has READ on refs/*: %s", refs);
- return new AutoValue_DefaultRefFilter_Result(refs, ImmutableList.of());
+ return new AutoValue_DefaultRefFilter_Result(
+ ImmutableList.copyOf(refs), ImmutableList.of());
} else if (projectControl.allRefsAreVisible(ImmutableSet.of(RefNames.REFS_CONFIG))) {
skipFilterCount.increment();
refs = fastHideRefsMetaConfig(refs);
logger.atFinest().log(
"Fast path, all refs except %s are visible: %s", RefNames.REFS_CONFIG, refs);
- return new AutoValue_DefaultRefFilter_Result(refs, ImmutableList.of());
+ return new AutoValue_DefaultRefFilter_Result(
+ ImmutableList.copyOf(refs), ImmutableList.of());
}
}
logger.atFinest().log("Doing full ref filtering");
@@ -263,7 +265,9 @@
resultRefs.add(ref);
}
}
- Result result = new AutoValue_DefaultRefFilter_Result(resultRefs, deferredTags);
+ Result result =
+ new AutoValue_DefaultRefFilter_Result(
+ ImmutableList.copyOf(resultRefs), ImmutableList.copyOf(deferredTags));
logger.atFinest().log("Result of ref filtering = %s", result);
return result;
}
@@ -401,12 +405,12 @@
@AutoValue
abstract static class Result {
/** Subset of the refs passed into the computation that is visible to the user. */
- abstract List<Ref> visibleRefs();
+ abstract ImmutableList<Ref> visibleRefs();
/**
* List of tags where we couldn't figure out visibility in the first pass and need to do an
* expensive ref walk.
*/
- abstract List<Ref> deferredTags();
+ abstract ImmutableList<Ref> deferredTags();
}
}
diff --git a/java/com/google/gerrit/server/permissions/SectionSortCache.java b/java/com/google/gerrit/server/permissions/SectionSortCache.java
index e64f8b6..621f1d0 100644
--- a/java/com/google/gerrit/server/permissions/SectionSortCache.java
+++ b/java/com/google/gerrit/server/permissions/SectionSortCache.java
@@ -125,7 +125,7 @@
abstract static class EntryKey {
public abstract String ref();
- public abstract List<String> patterns();
+ public abstract ImmutableList<String> patterns();
static EntryKey create(String refName, List<AccessSection> sections) {
List<String> patterns = new ArrayList<>(sections.size());
diff --git a/java/com/google/gerrit/server/plugincontext/PluginMapContext.java b/java/com/google/gerrit/server/plugincontext/PluginMapContext.java
index fb50cd5..cd61429 100644
--- a/java/com/google/gerrit/server/plugincontext/PluginMapContext.java
+++ b/java/com/google/gerrit/server/plugincontext/PluginMapContext.java
@@ -22,7 +22,7 @@
import com.google.gerrit.server.plugincontext.PluginContext.PluginMetrics;
import com.google.inject.Inject;
import java.util.Iterator;
-import java.util.SortedSet;
+import java.util.NavigableSet;
/**
* Context to invoke extensions from a {@link DynamicMap}.
@@ -135,7 +135,7 @@
* @return sorted list of the plugins that have registered implementations for this extension
* point
*/
- public SortedSet<String> plugins() {
+ public NavigableSet<String> plugins() {
return dynamicMap.plugins();
}
diff --git a/java/com/google/gerrit/server/plugins/JarPluginProvider.java b/java/com/google/gerrit/server/plugins/JarPluginProvider.java
index 82f97c9..c00a69d 100644
--- a/java/com/google/gerrit/server/plugins/JarPluginProvider.java
+++ b/java/com/google/gerrit/server/plugins/JarPluginProvider.java
@@ -29,9 +29,10 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
-import java.util.Date;
import java.util.List;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
@@ -104,8 +105,8 @@
}
private static String tempNameFor(String name) {
- SimpleDateFormat fmt = new SimpleDateFormat("yyMMdd_HHmm");
- return PLUGIN_TMP_PREFIX + name + "_" + fmt.format(new Date()) + "_";
+ DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyMMdd_HHmm").withZone(ZoneId.of("UTC"));
+ return PLUGIN_TMP_PREFIX + name + "_" + fmt.format(Instant.now()) + "_";
}
public static Path storeInTemp(String pluginName, InputStream in, SitePaths sitePaths)
diff --git a/java/com/google/gerrit/server/plugins/JarScanner.java b/java/com/google/gerrit/server/plugins/JarScanner.java
index 0f87135..e119bf1 100644
--- a/java/com/google/gerrit/server/plugins/JarScanner.java
+++ b/java/com/google/gerrit/server/plugins/JarScanner.java
@@ -15,12 +15,12 @@
package com.google.gerrit.server.plugins;
import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.Iterables.transform;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.MultimapBuilder;
import com.google.common.flogger.FluentLogger;
@@ -31,7 +31,6 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -42,6 +41,7 @@
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
+import java.util.stream.Stream;
import org.eclipse.jgit.util.IO;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
@@ -78,9 +78,7 @@
classObjToClassDescr.put(annotation, descriptor);
}
- Enumeration<JarEntry> e = jarFile.entries();
- while (e.hasMoreElements()) {
- JarEntry entry = e.nextElement();
+ for (JarEntry entry : entriesOf(jarFile)) {
if (skip(entry)) {
continue;
}
@@ -137,9 +135,7 @@
String name = superClass.replace('.', '/');
List<String> classes = new ArrayList<>();
- Enumeration<JarEntry> e = jarFile.entries();
- while (e.hasMoreElements()) {
- JarEntry entry = e.nextElement();
+ for (JarEntry entry : entriesOf(jarFile)) {
if (skip(entry)) {
continue;
}
@@ -294,10 +290,9 @@
}
@Override
- public Enumeration<PluginEntry> entries() {
- return Collections.enumeration(
- Lists.transform(
- Collections.list(jarFile.entries()),
+ public Stream<PluginEntry> entries() {
+ return jarFile.stream()
+ .map(
jarEntry -> {
try {
return resourceOf(jarEntry);
@@ -305,7 +300,7 @@
throw new IllegalArgumentException(
"Cannot convert jar entry " + jarEntry + " to a resource", e);
}
- }));
+ });
}
@Override
@@ -333,4 +328,8 @@
}
return Maps.transformEntries(attributes, (key, value) -> (String) value);
}
+
+ private static Iterable<JarEntry> entriesOf(JarFile jarFile) {
+ return jarFile.stream().collect(toImmutableList());
+ }
}
diff --git a/java/com/google/gerrit/server/plugins/PluginContentScanner.java b/java/com/google/gerrit/server/plugins/PluginContentScanner.java
index b19d6de..12c06f3 100644
--- a/java/com/google/gerrit/server/plugins/PluginContentScanner.java
+++ b/java/com/google/gerrit/server/plugins/PluginContentScanner.java
@@ -19,10 +19,10 @@
import java.lang.annotation.Annotation;
import java.nio.file.NoSuchFileException;
import java.util.Collections;
-import java.util.Enumeration;
import java.util.Map;
import java.util.Optional;
import java.util.jar.Manifest;
+import java.util.stream.Stream;
/**
* Scans the plugin returning classes and resources.
@@ -58,8 +58,8 @@
}
@Override
- public Enumeration<PluginEntry> entries() {
- return Collections.emptyEnumeration();
+ public Stream<PluginEntry> entries() {
+ return Stream.empty();
}
};
@@ -122,5 +122,5 @@
*
* @return the enumeration of all resources found
*/
- Enumeration<PluginEntry> entries();
+ Stream<PluginEntry> entries();
}
diff --git a/java/com/google/gerrit/server/plugins/PluginResource.java b/java/com/google/gerrit/server/plugins/PluginResource.java
index e7ebd56..fb69233 100644
--- a/java/com/google/gerrit/server/plugins/PluginResource.java
+++ b/java/com/google/gerrit/server/plugins/PluginResource.java
@@ -19,8 +19,7 @@
import com.google.inject.TypeLiteral;
public class PluginResource implements RestResource {
- public static final TypeLiteral<RestView<PluginResource>> PLUGIN_KIND =
- new TypeLiteral<RestView<PluginResource>>() {};
+ public static final TypeLiteral<RestView<PluginResource>> PLUGIN_KIND = new TypeLiteral<>() {};
private final Plugin plugin;
private final String name;
diff --git a/java/com/google/gerrit/server/plugins/PluginScannerThread.java b/java/com/google/gerrit/server/plugins/PluginScannerThread.java
index 705e3c0..1c2c836 100644
--- a/java/com/google/gerrit/server/plugins/PluginScannerThread.java
+++ b/java/com/google/gerrit/server/plugins/PluginScannerThread.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.plugins;
+import com.google.common.util.concurrent.Uninterruptibles;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -45,10 +46,6 @@
void end() {
done.countDown();
- try {
- join();
- } catch (InterruptedException e) {
- // Ignored
- }
+ Uninterruptibles.joinUninterruptibly(this);
}
}
diff --git a/java/com/google/gerrit/server/plugins/TestServerPlugin.java b/java/com/google/gerrit/server/plugins/TestServerPlugin.java
index 3751c3f..cd5d5e3 100644
--- a/java/com/google/gerrit/server/plugins/TestServerPlugin.java
+++ b/java/com/google/gerrit/server/plugins/TestServerPlugin.java
@@ -28,7 +28,7 @@
String name,
String pluginCanonicalWebUrl,
PluginUser user,
- ClassLoader classloader,
+ ClassLoader classLoader,
String sysName,
String httpName,
String sshName,
@@ -42,10 +42,10 @@
null,
null,
dataDir,
- classloader,
+ classLoader,
null,
GerritRuntime.DAEMON);
- this.classLoader = classloader;
+ this.classLoader = classLoader;
this.sysName = sysName;
this.httpName = httpName;
this.sshName = sshName;
diff --git a/java/com/google/gerrit/server/project/BranchResource.java b/java/com/google/gerrit/server/project/BranchResource.java
index a8936ac..f071fbe 100644
--- a/java/com/google/gerrit/server/project/BranchResource.java
+++ b/java/com/google/gerrit/server/project/BranchResource.java
@@ -21,8 +21,7 @@
import org.eclipse.jgit.lib.Ref;
public class BranchResource extends RefResource {
- public static final TypeLiteral<RestView<BranchResource>> BRANCH_KIND =
- new TypeLiteral<RestView<BranchResource>>() {};
+ public static final TypeLiteral<RestView<BranchResource>> BRANCH_KIND = new TypeLiteral<>() {};
private final String refName;
private final String revision;
diff --git a/java/com/google/gerrit/server/project/ChildProjectResource.java b/java/com/google/gerrit/server/project/ChildProjectResource.java
index 4b641ca..854f876 100644
--- a/java/com/google/gerrit/server/project/ChildProjectResource.java
+++ b/java/com/google/gerrit/server/project/ChildProjectResource.java
@@ -21,7 +21,7 @@
public class ChildProjectResource implements RestResource {
public static final TypeLiteral<RestView<ChildProjectResource>> CHILD_PROJECT_KIND =
- new TypeLiteral<RestView<ChildProjectResource>>() {};
+ new TypeLiteral<>() {};
private final ProjectResource parent;
private final ProjectState child;
diff --git a/java/com/google/gerrit/server/project/CommitResource.java b/java/com/google/gerrit/server/project/CommitResource.java
index f71c7fe..ffd498d 100644
--- a/java/com/google/gerrit/server/project/CommitResource.java
+++ b/java/com/google/gerrit/server/project/CommitResource.java
@@ -20,8 +20,7 @@
import org.eclipse.jgit.revwalk.RevCommit;
public class CommitResource implements RestResource {
- public static final TypeLiteral<RestView<CommitResource>> COMMIT_KIND =
- new TypeLiteral<RestView<CommitResource>>() {};
+ public static final TypeLiteral<RestView<CommitResource>> COMMIT_KIND = new TypeLiteral<>() {};
private final ProjectResource project;
private final RevCommit commit;
diff --git a/java/com/google/gerrit/server/project/DashboardResource.java b/java/com/google/gerrit/server/project/DashboardResource.java
index 54f958a..c918551 100644
--- a/java/com/google/gerrit/server/project/DashboardResource.java
+++ b/java/com/google/gerrit/server/project/DashboardResource.java
@@ -22,7 +22,7 @@
public class DashboardResource implements RestResource {
public static final TypeLiteral<RestView<DashboardResource>> DASHBOARD_KIND =
- new TypeLiteral<RestView<DashboardResource>>() {};
+ new TypeLiteral<>() {};
public static DashboardResource projectDefault(ProjectState projectState, CurrentUser user) {
return new DashboardResource(projectState, user, null, null, null, true);
diff --git a/java/com/google/gerrit/server/project/FileResource.java b/java/com/google/gerrit/server/project/FileResource.java
index e8926dc..f02522a 100644
--- a/java/com/google/gerrit/server/project/FileResource.java
+++ b/java/com/google/gerrit/server/project/FileResource.java
@@ -33,8 +33,7 @@
* <p>This is in the project package because it is accessed through the project/branch/file path.
*/
public class FileResource implements RestResource {
- public static final TypeLiteral<RestView<FileResource>> FILE_KIND =
- new TypeLiteral<RestView<FileResource>>() {};
+ public static final TypeLiteral<RestView<FileResource>> FILE_KIND = new TypeLiteral<>() {};
public static FileResource create(
GitRepositoryManager repoManager, ProjectState projectState, ObjectId rev, String path)
diff --git a/java/com/google/gerrit/server/project/LabelDefinitionJson.java b/java/com/google/gerrit/server/project/LabelDefinitionJson.java
index 63c9d22..71ea12b 100644
--- a/java/com/google/gerrit/server/project/LabelDefinitionJson.java
+++ b/java/com/google/gerrit/server/project/LabelDefinitionJson.java
@@ -25,6 +25,7 @@
public static LabelDefinitionInfo format(Project.NameKey projectName, LabelType labelType) {
LabelDefinitionInfo label = new LabelDefinitionInfo();
label.name = labelType.getName();
+ label.description = labelType.getDescription().orElse(null);
label.projectName = projectName.get();
label.function = labelType.getFunction().getFunctionName();
label.values =
diff --git a/java/com/google/gerrit/server/project/LabelResource.java b/java/com/google/gerrit/server/project/LabelResource.java
index 2df9ff1..fcddc61 100644
--- a/java/com/google/gerrit/server/project/LabelResource.java
+++ b/java/com/google/gerrit/server/project/LabelResource.java
@@ -20,8 +20,7 @@
import com.google.inject.TypeLiteral;
public class LabelResource implements RestResource {
- public static final TypeLiteral<RestView<LabelResource>> LABEL_KIND =
- new TypeLiteral<RestView<LabelResource>>() {};
+ public static final TypeLiteral<RestView<LabelResource>> LABEL_KIND = new TypeLiteral<>() {};
private final ProjectResource project;
private final LabelType labelType;
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 7e7c7be..31bbff5 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -130,7 +130,7 @@
.keySerializer(new ProtobufSerializer<>(Cache.ProjectCacheKeyProto.parser()))
.valueSerializer(PersistedProjectConfigSerializer.INSTANCE)
.diskLimit(1 << 30) // 1 GiB
- .version(2)
+ .version(3)
.maximumWeight(0);
cache(CACHE_LIST, ListKey.class, new TypeLiteral<ImmutableSortedSet<Project.NameKey>>() {})
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 8f0b535..03886a9 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -23,11 +23,13 @@
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
@@ -104,6 +106,7 @@
public static final String COMMENTLINK = "commentlink";
public static final String LABEL = "label";
+ public static final String KEY_LABEL_DESCRIPTION = "description";
public static final String KEY_FUNCTION = "function";
public static final String KEY_DEFAULT_VALUE = "defaultValue";
public static final String KEY_COPY_MIN_SCORE = "copyMinScore";
@@ -130,6 +133,13 @@
public static final String KEY_SR_SUBMITTABILITY_EXPRESSION = "submittableIf";
public static final String KEY_SR_OVERRIDE_EXPRESSION = "overrideIf";
public static final String KEY_SR_OVERRIDE_IN_CHILD_PROJECTS = "canOverrideInChildProjects";
+ public static final ImmutableSet<String> SR_KEYS =
+ ImmutableSet.of(
+ KEY_SR_DESCRIPTION,
+ KEY_SR_APPLICABILITY_EXPRESSION,
+ KEY_SR_SUBMITTABILITY_EXPRESSION,
+ KEY_SR_OVERRIDE_EXPRESSION,
+ KEY_SR_OVERRIDE_IN_CHILD_PROJECTS);
public static final String KEY_MATCH = "match";
private static final String KEY_HTML = "html";
@@ -523,6 +533,11 @@
submitRequirementSections.put(requirement.name(), requirement);
}
+ @VisibleForTesting
+ public void clearSubmitRequirements() {
+ submitRequirementSections = new LinkedHashMap<>();
+ }
+
/** Adds or replaces the given {@link LabelType} in this config. */
public void upsertLabelType(LabelType labelType) {
labelSections.put(labelType.getName(), labelType);
@@ -936,6 +951,8 @@
}
private void loadSubmitRequirementSections(Config rc) {
+ checkForUnsupportedSubmitRequirementParams(rc);
+
Map<String, String> lowerNames = new HashMap<>();
submitRequirementSections = new LinkedHashMap<>();
for (String name : rc.getSubsections(SUBMIT_REQUIREMENT)) {
@@ -948,28 +965,46 @@
}
lowerNames.put(lower, name);
String description = rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_DESCRIPTION);
- String appExpr = rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_APPLICABILITY_EXPRESSION);
- String blockExpr = rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_SUBMITTABILITY_EXPRESSION);
+ String applicabilityExpr =
+ rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_APPLICABILITY_EXPRESSION);
+ String submittabilityExpr =
+ rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_SUBMITTABILITY_EXPRESSION);
String overrideExpr = rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_OVERRIDE_EXPRESSION);
- boolean canInherit =
- rc.getBoolean(SUBMIT_REQUIREMENT, name, KEY_SR_OVERRIDE_IN_CHILD_PROJECTS, false);
-
- if (blockExpr == null) {
+ boolean canInherit;
+ try {
+ canInherit =
+ rc.getBoolean(SUBMIT_REQUIREMENT, name, KEY_SR_OVERRIDE_IN_CHILD_PROJECTS, false);
+ } catch (IllegalArgumentException e) {
+ String canInheritValue =
+ rc.getString(SUBMIT_REQUIREMENT, name, KEY_SR_OVERRIDE_IN_CHILD_PROJECTS);
error(
String.format(
- "Submit requirement '%s' does not define a submittability expression.", name));
+ "Invalid value %s.%s.%s for submit requirement '%s': %s",
+ SUBMIT_REQUIREMENT,
+ name,
+ KEY_SR_OVERRIDE_IN_CHILD_PROJECTS,
+ name,
+ canInheritValue));
continue;
}
- // TODO(SR): add expressions validation. Expressions are stored as strings so we need to
- // validate their syntax.
+ if (submittabilityExpr == null) {
+ error(
+ String.format(
+ "Setting a submittability expression for submit requirement '%s' is required:"
+ + " Missing %s.%s.%s",
+ name, SUBMIT_REQUIREMENT, name, KEY_SR_SUBMITTABILITY_EXPRESSION));
+ continue;
+ }
+
+ // The expressions are validated in SubmitRequirementExpressionsValidator.
SubmitRequirement submitRequirement =
SubmitRequirement.builder()
.setName(name)
.setDescription(Optional.ofNullable(description))
- .setApplicabilityExpression(SubmitRequirementExpression.of(appExpr))
- .setSubmittabilityExpression(SubmitRequirementExpression.create(blockExpr))
+ .setApplicabilityExpression(SubmitRequirementExpression.of(applicabilityExpr))
+ .setSubmittabilityExpression(SubmitRequirementExpression.create(submittabilityExpr))
.setOverrideExpression(SubmitRequirementExpression.of(overrideExpr))
.setAllowOverrideInChildProjects(canInherit)
.build();
@@ -978,6 +1013,43 @@
}
}
+ /**
+ * Report unsupported submit requirement parameters as errors.
+ *
+ * <p>Unsupported are submit requirements parameters that
+ *
+ * <ul>
+ * <li>are directly set in the {@code submit-requirement} section (as submit requirements are
+ * solely defined in subsections)
+ * <li>are unknown (maybe they were accidentally misspelled?)
+ * </ul>
+ */
+ private void checkForUnsupportedSubmitRequirementParams(Config rc) {
+ Set<String> directSubmitRequirementParams = rc.getNames(SUBMIT_REQUIREMENT);
+ if (!directSubmitRequirementParams.isEmpty()) {
+ error(
+ String.format(
+ "Submit requirements must be defined in %s.<name> subsections."
+ + " Setting parameters directly in the %s section is not allowed: %s",
+ SUBMIT_REQUIREMENT,
+ SUBMIT_REQUIREMENT,
+ directSubmitRequirementParams.stream().sorted().collect(toImmutableList())));
+ }
+
+ for (String subsection : rc.getSubsections(SUBMIT_REQUIREMENT)) {
+ ImmutableList<String> unknownSubmitRequirementParams =
+ rc.getNames(SUBMIT_REQUIREMENT, subsection).stream()
+ .filter(p -> !SR_KEYS.contains(p))
+ .collect(toImmutableList());
+ if (!unknownSubmitRequirementParams.isEmpty()) {
+ error(
+ String.format(
+ "Unsupported parameters for submit requirement '%s': %s",
+ subsection, unknownSubmitRequirementParams));
+ }
+ }
+ }
+
private void loadLabelSections(Config rc) {
Map<String, String> lowerNames = Maps.newHashMapWithExpectedSize(2);
labelSections = new LinkedHashMap<>();
@@ -1014,6 +1086,8 @@
continue;
}
+ label.setDescription(Optional.ofNullable(rc.getString(LABEL, name, KEY_LABEL_DESCRIPTION)));
+
String functionName = rc.getString(LABEL, name, KEY_FUNCTION);
Optional<LabelFunction> function =
functionName != null
@@ -1095,7 +1169,23 @@
label.setCanOverride(
rc.getBoolean(LABEL, name, KEY_CAN_OVERRIDE, LabelType.DEF_CAN_OVERRIDE));
List<String> refPatterns = getStringListOrNull(rc, LABEL, name, KEY_BRANCH);
- label.setRefPatterns(refPatterns == null ? null : ImmutableList.copyOf(refPatterns));
+ if (refPatterns == null) {
+ label.setRefPatterns(null);
+ } else {
+ for (String pattern : refPatterns) {
+ if (pattern.startsWith("^")) {
+ try {
+ Pattern.compile(pattern);
+ } catch (PatternSyntaxException e) {
+ error(
+ String.format(
+ "Invalid ref pattern \"%s\" in %s.%s.%s: %s",
+ pattern, LABEL, name, KEY_BRANCH, e.getMessage()));
+ }
+ }
+ }
+ label.setRefPatterns(ImmutableList.copyOf(refPatterns));
+ }
labelSections.put(name, label.build());
}
}
@@ -1516,6 +1606,11 @@
String name = e.getKey();
LabelType label = e.getValue();
toUnset.remove(name);
+ if (label.getDescription().isPresent() && !label.getDescription().get().isEmpty()) {
+ rc.setString(LABEL, name, KEY_LABEL_DESCRIPTION, label.getDescription().get());
+ } else {
+ rc.unset(LABEL, name, KEY_LABEL_DESCRIPTION);
+ }
rc.setString(LABEL, name, KEY_FUNCTION, label.getFunction().getFunctionName());
rc.setInt(LABEL, name, KEY_DEFAULT_VALUE, label.getDefaultValue());
diff --git a/java/com/google/gerrit/server/project/ProjectCreator.java b/java/com/google/gerrit/server/project/ProjectCreator.java
index 4e778a4..f1c161d 100644
--- a/java/com/google/gerrit/server/project/ProjectCreator.java
+++ b/java/com/google/gerrit/server/project/ProjectCreator.java
@@ -135,10 +135,6 @@
e);
} catch (RepositoryNotFoundException badName) {
throw new BadRequestException("invalid project name: " + nameKey, badName);
- } catch (ConfigInvalidException e) {
- String msg = "Cannot create " + nameKey;
- logger.atSevere().withCause(e).log(msg);
- throw e;
}
}
diff --git a/java/com/google/gerrit/server/project/ProjectResource.java b/java/com/google/gerrit/server/project/ProjectResource.java
index 8802758..f9fa22e 100644
--- a/java/com/google/gerrit/server/project/ProjectResource.java
+++ b/java/com/google/gerrit/server/project/ProjectResource.java
@@ -21,8 +21,7 @@
import com.google.inject.TypeLiteral;
public class ProjectResource implements RestResource {
- public static final TypeLiteral<RestView<ProjectResource>> PROJECT_KIND =
- new TypeLiteral<RestView<ProjectResource>>() {};
+ public static final TypeLiteral<RestView<ProjectResource>> PROJECT_KIND = new TypeLiteral<>() {};
private final ProjectState projectState;
private final CurrentUser user;
diff --git a/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
index fca1b36..39d9aec7 100644
--- a/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
+++ b/java/com/google/gerrit/server/project/ProjectsConsistencyChecker.java
@@ -247,7 +247,7 @@
return r;
}
- private List<ChangeInfo> executeQueryAndAutoCloseChanges(
+ private ImmutableList<ChangeInfo> executeQueryAndAutoCloseChanges(
Predicate<ChangeData> basePredicate,
Set<Change.Id> seenChanges,
List<Predicate<ChangeData>> predicates,
@@ -306,7 +306,7 @@
}
}
- return autoCloseableChangesByBranch;
+ return ImmutableList.copyOf(autoCloseableChangesByBranch);
} catch (Exception e) {
Throwables.throwIfUnchecked(e);
throw new StorageException(e);
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementExpressionsValidator.java b/java/com/google/gerrit/server/project/SubmitRequirementExpressionsValidator.java
new file mode 100644
index 0000000..8717581
--- /dev/null
+++ b/java/com/google/gerrit/server/project/SubmitRequirementExpressionsValidator.java
@@ -0,0 +1,182 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementExpression;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.git.validators.ValidationMessage;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
+import com.google.gerrit.server.patch.DiffOperations;
+import com.google.gerrit.server.patch.DiffOptions;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/**
+ * Validates the expressions of submit requirements in {@code project.config}.
+ *
+ * <p>Other validation of submit requirements is done in {@link ProjectConfig}, see {@code
+ * ProjectConfig#loadSubmitRequirementSections(Config)}.
+ *
+ * <p>The validation of the expressions cannot be in {@link ProjectConfig} as it requires injecting
+ * {@link SubmitRequirementsEvaluator} and we cannot do injections into {@link ProjectConfig} (since
+ * {@link ProjectConfig} is cached in the project cache).
+ */
+public class SubmitRequirementExpressionsValidator implements CommitValidationListener {
+ private final DiffOperations diffOperations;
+ private final ProjectConfig.Factory projectConfigFactory;
+ private final SubmitRequirementsEvaluator submitRequirementsEvaluator;
+
+ @Inject
+ SubmitRequirementExpressionsValidator(
+ DiffOperations diffOperations,
+ ProjectConfig.Factory projectConfigFactory,
+ SubmitRequirementsEvaluator submitRequirementsEvaluator) {
+ this.diffOperations = diffOperations;
+ this.projectConfigFactory = projectConfigFactory;
+ this.submitRequirementsEvaluator = submitRequirementsEvaluator;
+ }
+
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent event)
+ throws CommitValidationException {
+ try {
+ if (!event.refName.equals(RefNames.REFS_CONFIG)
+ || !isFileChanged(event, ProjectConfig.PROJECT_CONFIG)) {
+ // the project.config file in refs/meta/config was not modified, hence we do not need to
+ // validate the submit requirements in it
+ return ImmutableList.of();
+ }
+
+ ProjectConfig projectConfig = getProjectConfig(event);
+ ImmutableList<CommitValidationMessage> validationMessages =
+ validateSubmitRequirementExpressions(
+ projectConfig.getSubmitRequirementSections().values());
+ if (!validationMessages.isEmpty()) {
+ throw new CommitValidationException(
+ String.format(
+ "invalid submit requirement expressions in %s (revision = %s)",
+ ProjectConfig.PROJECT_CONFIG, projectConfig.getRevision()),
+ validationMessages);
+ }
+ return ImmutableList.of();
+ } catch (IOException | DiffNotAvailableException | ConfigInvalidException e) {
+ throw new CommitValidationException(
+ String.format(
+ "failed to validate submit requirement expressions in %s for revision %s in ref %s"
+ + " of project %s",
+ ProjectConfig.PROJECT_CONFIG,
+ event.commit.getName(),
+ RefNames.REFS_CONFIG,
+ event.project.getNameKey()),
+ e);
+ }
+ }
+
+ /**
+ * Whether the given file was changed in the given revision.
+ *
+ * @param receiveEvent the receive event
+ * @param fileName the name of the file
+ */
+ private boolean isFileChanged(CommitReceivedEvent receiveEvent, String fileName)
+ throws DiffNotAvailableException {
+ return diffOperations
+ .listModifiedFilesAgainstParent(
+ receiveEvent.project.getNameKey(),
+ receiveEvent.commit,
+ /* parentNum=*/ 0,
+ DiffOptions.DEFAULTS)
+ .keySet().stream()
+ .anyMatch(fileName::equals);
+ }
+
+ private ProjectConfig getProjectConfig(CommitReceivedEvent receiveEvent)
+ throws IOException, ConfigInvalidException {
+ ProjectConfig projectConfig = projectConfigFactory.create(receiveEvent.project.getNameKey());
+ projectConfig.load(receiveEvent.revWalk, receiveEvent.commit);
+ return projectConfig;
+ }
+
+ private ImmutableList<CommitValidationMessage> validateSubmitRequirementExpressions(
+ Collection<SubmitRequirement> submitRequirements) {
+ List<CommitValidationMessage> validationMessages = new ArrayList<>();
+ for (SubmitRequirement submitRequirement : submitRequirements) {
+ validateSubmitRequirementExpression(
+ validationMessages,
+ submitRequirement,
+ submitRequirement.submittabilityExpression(),
+ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION);
+ submitRequirement
+ .applicabilityExpression()
+ .ifPresent(
+ expression ->
+ validateSubmitRequirementExpression(
+ validationMessages,
+ submitRequirement,
+ expression,
+ ProjectConfig.KEY_SR_APPLICABILITY_EXPRESSION));
+ submitRequirement
+ .overrideExpression()
+ .ifPresent(
+ expression ->
+ validateSubmitRequirementExpression(
+ validationMessages,
+ submitRequirement,
+ expression,
+ ProjectConfig.KEY_SR_OVERRIDE_EXPRESSION));
+ }
+ return ImmutableList.copyOf(validationMessages);
+ }
+
+ private void validateSubmitRequirementExpression(
+ List<CommitValidationMessage> validationMessages,
+ SubmitRequirement submitRequirement,
+ SubmitRequirementExpression expression,
+ String configKey) {
+ try {
+ submitRequirementsEvaluator.validateExpression(expression);
+ } catch (QueryParseException e) {
+ if (validationMessages.isEmpty()) {
+ validationMessages.add(
+ new CommitValidationMessage(
+ "Invalid project configuration", ValidationMessage.Type.ERROR));
+ }
+ validationMessages.add(
+ new CommitValidationMessage(
+ String.format(
+ " %s: Expression '%s' of submit requirement '%s' (parameter %s.%s.%s) is"
+ + " invalid: %s",
+ ProjectConfig.PROJECT_CONFIG,
+ expression.expressionString(),
+ submitRequirement.name(),
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ submitRequirement.name(),
+ configKey,
+ e.getMessage()),
+ ValidationMessage.Type.ERROR));
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java b/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
index 539edc1..2a2e67d 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
@@ -14,8 +14,8 @@
package com.google.gerrit.server.project;
+import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.MoreCollectors;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.SubmitRecord;
@@ -48,36 +48,69 @@
* com.google.gerrit.server.rules.SubmitRule}s) and convert them to submit requirement results.
*/
public static Map<SubmitRequirement, SubmitRequirementResult> getLegacyRequirements(
- SubmitRuleEvaluator.Factory evaluator, ChangeData cd) {
+ ChangeData cd) {
// We use SubmitRuleOptions.defaults() which does not recompute submit rules for closed changes.
// This doesn't have an effect since we never call this class (i.e. to evaluate submit
// requirements) for closed changes.
- List<SubmitRecord> records = evaluator.create(SubmitRuleOptions.defaults()).evaluate(cd);
+ List<SubmitRecord> records = cd.submitRecords(SubmitRuleOptions.defaults());
+ boolean areForced =
+ records.stream().anyMatch(record -> SubmitRecord.Status.FORCED.equals(record.status));
List<LabelType> labelTypes = cd.getLabelTypes().getLabelTypes();
ObjectId commitId = cd.currentPatchSet().commitId();
return records.stream()
- .map(r -> createResult(r, labelTypes, commitId))
+ // Filter out the "FORCED" submit record. This is a marker submit record that was just used
+ // to indicate that all other records were forced. "FORCED" means that the change was pushed
+ // with the %submit option bypassing submit rules.
+ .filter(r -> !SubmitRecord.Status.FORCED.equals(r.status))
+ .map(r -> createResult(r, labelTypes, commitId, areForced))
.flatMap(List::stream)
- .collect(Collectors.toMap(sr -> sr.submitRequirement(), Function.identity()));
+ .collect(
+ Collectors.toMap(
+ sr -> sr.submitRequirement(),
+ Function.identity(),
+ (r1, r2) -> {
+ // We convert submit records to submit requirements by generating a separate
+ // submit requirement result for each available label in each submit record.
+ // The SR status is derived from the label status of the submit record.
+ // This conversion might result in duplicate entries.
+ // One such example can be a prolog rule emitting the same label name twice.
+ // Another case might happen if two different submit rules emit the same label
+ // name. In such cases, we need to merge these entries and return a single submit
+ // requirement result. If both entries agree in their status, return any of them.
+ // Otherwise, favour the entry that is blocking submission.
+ if (r1.fulfilled() == r2.fulfilled()) {
+ return r1;
+ }
+ return r1.fulfilled() ? r2 : r1;
+ }));
}
static List<SubmitRequirementResult> createResult(
- SubmitRecord record, List<LabelType> labelTypes, ObjectId psCommitId) {
+ SubmitRecord record, List<LabelType> labelTypes, ObjectId psCommitId, boolean isForced) {
List<SubmitRequirementResult> results;
if (record.ruleName != null && record.ruleName.equals("gerrit~DefaultSubmitRule")) {
- results = createFromDefaultSubmitRecord(record.labels, labelTypes, psCommitId);
+ results = createFromDefaultSubmitRecord(record.labels, labelTypes, psCommitId, isForced);
} else {
- results = createFromCustomSubmitRecord(record, psCommitId);
+ results = createFromCustomSubmitRecord(record, psCommitId, isForced);
}
logger.atFine().log("Converted submit record %s to submit requirements %s", record, results);
return results;
}
private static List<SubmitRequirementResult> createFromDefaultSubmitRecord(
- List<Label> labels, List<LabelType> labelTypes, ObjectId psCommitId) {
+ List<Label> labels, List<LabelType> labelTypes, ObjectId psCommitId, boolean isForced) {
ImmutableList.Builder<SubmitRequirementResult> result = ImmutableList.builder();
for (Label label : labels) {
- LabelType labelType = getLabelType(labelTypes, label.label);
+ if (skipSubmitRequirementFor(label)) {
+ continue;
+ }
+ Optional<LabelType> maybeLabelType = getLabelType(labelTypes, label.label);
+ if (!maybeLabelType.isPresent()) {
+ // Label type might have been removed from the project config. We don't have information
+ // if it was blocking or not, hence we skip the label.
+ continue;
+ }
+ LabelType labelType = maybeLabelType.get();
if (!isBlocking(labelType)) {
continue;
}
@@ -94,13 +127,14 @@
.submittabilityExpressionResult(
createExpressionResult(toExpression(atoms), mapStatus(label), atoms))
.patchSetCommitId(psCommitId)
+ .forced(Optional.of(isForced))
.build());
}
return result.build();
}
private static List<SubmitRequirementResult> createFromCustomSubmitRecord(
- SubmitRecord record, ObjectId psCommitId) {
+ SubmitRecord record, ObjectId psCommitId, boolean isForced) {
String ruleName = record.ruleName != null ? record.ruleName : "Custom-Rule";
if (record.labels == null || record.labels.isEmpty()) {
SubmitRequirement sr =
@@ -116,12 +150,19 @@
.submitRequirement(sr)
.submittabilityExpressionResult(
createExpressionResult(
- sr.submittabilityExpression(), mapStatus(record), ImmutableList.of(ruleName)))
+ sr.submittabilityExpression(),
+ mapStatus(record),
+ ImmutableList.of(ruleName),
+ record.errorMessage))
.patchSetCommitId(psCommitId)
+ .forced(Optional.of(isForced))
.build());
}
ImmutableList.Builder<SubmitRequirementResult> result = ImmutableList.builder();
for (Label label : record.labels) {
+ if (skipSubmitRequirementFor(label)) {
+ continue;
+ }
String expressionString = String.format("label:%s=%s", label.label, ruleName);
SubmitRequirement sr =
SubmitRequirement.builder()
@@ -212,9 +253,40 @@
status == Status.FAIL ? atoms : ImmutableList.of());
}
- private static LabelType getLabelType(List<LabelType> labelTypes, String labelName) {
- return labelTypes.stream()
- .filter(lt -> lt.getName().equals(labelName))
- .collect(MoreCollectors.onlyElement());
+ private static SubmitRequirementExpressionResult createExpressionResult(
+ SubmitRequirementExpression expression,
+ Status status,
+ ImmutableList<String> atoms,
+ String errorMessage) {
+ return SubmitRequirementExpressionResult.create(
+ expression,
+ status,
+ status == Status.PASS ? atoms : ImmutableList.of(),
+ status == Status.FAIL ? atoms : ImmutableList.of(),
+ Optional.ofNullable(Strings.emptyToNull(errorMessage)));
+ }
+
+ private static Optional<LabelType> getLabelType(List<LabelType> labelTypes, String labelName) {
+ List<LabelType> label =
+ labelTypes.stream()
+ .filter(lt -> lt.getName().equals(labelName))
+ .collect(Collectors.toList());
+ if (label.isEmpty()) {
+ // Label might have been removed from the project.
+ logger.atFine().log("Label '%s' was not found for the project.", labelName);
+ return Optional.empty();
+ } else if (label.size() > 1) {
+ logger.atWarning().log("Found more than one label definition for label name '%s'", labelName);
+ return Optional.empty();
+ }
+ return Optional.of(label.get(0));
+ }
+
+ /**
+ * Returns true if we should skip creating a "submit requirement" result out of the "submit
+ * record" label.
+ */
+ private static boolean skipSubmitRequirementFor(SubmitRecord.Label label) {
+ return label.status == SubmitRecord.Label.Status.MAY;
}
}
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
index cc2c805..64c9a4c 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsEvaluatorImpl.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.project;
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import com.google.common.collect.ImmutableMap;
@@ -24,8 +25,11 @@
import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.SubmitRequirementChangeQueryBuilder;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.OneOffRequestContext;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Module;
@@ -34,13 +38,17 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Stream;
/** Evaluates submit requirements for different change data. */
public class SubmitRequirementsEvaluatorImpl implements SubmitRequirementsEvaluator {
private final Provider<SubmitRequirementChangeQueryBuilder> queryBuilder;
private final ProjectCache projectCache;
- private final SubmitRuleEvaluator.Factory legacyEvaluator;
+ private final PluginSetContext<SubmitRequirement> globalSubmitRequirements;
+ private final SubmitRequirementsUtil submitRequirementsUtil;
+ private final OneOffRequestContext requestContext;
public static Module module() {
return new AbstractModule() {
@@ -57,10 +65,14 @@
private SubmitRequirementsEvaluatorImpl(
Provider<SubmitRequirementChangeQueryBuilder> queryBuilder,
ProjectCache projectCache,
- SubmitRuleEvaluator.Factory legacyEvaluator) {
+ PluginSetContext<SubmitRequirement> globalSubmitRequirements,
+ SubmitRequirementsUtil submitRequirementsUtil,
+ OneOffRequestContext requestContext) {
this.queryBuilder = queryBuilder;
this.projectCache = projectCache;
- this.legacyEvaluator = legacyEvaluator;
+ this.globalSubmitRequirements = globalSubmitRequirements;
+ this.submitRequirementsUtil = submitRequirementsUtil;
+ this.requestContext = requestContext;
}
@Override
@@ -76,37 +88,42 @@
Map<SubmitRequirement, SubmitRequirementResult> result = projectConfigRequirements;
if (includeLegacy) {
Map<SubmitRequirement, SubmitRequirementResult> legacyReqs =
- SubmitRequirementsAdapter.getLegacyRequirements(legacyEvaluator, cd);
+ SubmitRequirementsAdapter.getLegacyRequirements(cd);
result =
- SubmitRequirementsUtil.mergeLegacyAndNonLegacyRequirements(
- projectConfigRequirements, legacyReqs);
+ submitRequirementsUtil.mergeLegacyAndNonLegacyRequirements(
+ projectConfigRequirements, legacyReqs, cd.project());
}
return ImmutableMap.copyOf(result);
}
@Override
public SubmitRequirementResult evaluateRequirement(SubmitRequirement sr, ChangeData cd) {
- SubmitRequirementExpressionResult blockingResult =
- evaluateExpression(sr.submittabilityExpression(), cd);
+ try (ManualRequestContext ignored = requestContext.open()) {
+ // Use a request context to execute predicates as an internal user with expanded visibility.
+ // This is so that the evaluation does not depend on who is running the current request (e.g.
+ // a "ownerin" predicate with group that is not visible to the person making this request).
+ SubmitRequirementExpressionResult blockingResult =
+ evaluateExpression(sr.submittabilityExpression(), cd);
- Optional<SubmitRequirementExpressionResult> applicabilityResult =
- sr.applicabilityExpression().isPresent()
- ? Optional.of(evaluateExpression(sr.applicabilityExpression().get(), cd))
- : Optional.empty();
+ Optional<SubmitRequirementExpressionResult> applicabilityResult =
+ sr.applicabilityExpression().isPresent()
+ ? Optional.of(evaluateExpression(sr.applicabilityExpression().get(), cd))
+ : Optional.empty();
- Optional<SubmitRequirementExpressionResult> overrideResult =
- sr.overrideExpression().isPresent()
- ? Optional.of(evaluateExpression(sr.overrideExpression().get(), cd))
- : Optional.empty();
+ Optional<SubmitRequirementExpressionResult> overrideResult =
+ sr.overrideExpression().isPresent()
+ ? Optional.of(evaluateExpression(sr.overrideExpression().get(), cd))
+ : Optional.empty();
- return SubmitRequirementResult.builder()
- .legacy(Optional.of(false))
- .submitRequirement(sr)
- .patchSetCommitId(cd.currentPatchSet().commitId())
- .submittabilityExpressionResult(blockingResult)
- .applicabilityExpressionResult(applicabilityResult)
- .overrideExpressionResult(overrideResult)
- .build();
+ return SubmitRequirementResult.builder()
+ .legacy(Optional.of(false))
+ .submitRequirement(sr)
+ .patchSetCommitId(cd.currentPatchSet().commitId())
+ .submittabilityExpressionResult(blockingResult)
+ .applicabilityExpressionResult(applicabilityResult)
+ .overrideExpressionResult(overrideResult)
+ .build();
+ }
}
@Override
@@ -121,15 +138,52 @@
}
}
- /** Evaluate and return submit requirements stored in this project's config and its parents. */
+ /**
+ * Evaluate and return all {@link SubmitRequirement}s.
+ *
+ * <p>This includes all globally bound {@link SubmitRequirement}s, as well as requirements stored
+ * in this project's config and its parents.
+ *
+ * <p>The behaviour in case of the name match is controlled by {@link
+ * SubmitRequirement#allowOverrideInChildProjects} of global {@link SubmitRequirement}.
+ */
private Map<SubmitRequirement, SubmitRequirementResult> getRequirements(ChangeData cd) {
+ Map<String, SubmitRequirement> globalRequirements = getGlobalRequirements();
+
ProjectState state = projectCache.get(cd.project()).orElseThrow(illegalState(cd.project()));
- Map<String, SubmitRequirement> requirements = state.getSubmitRequirements();
- Map<SubmitRequirement, SubmitRequirementResult> result = new HashMap<>();
+ Map<String, SubmitRequirement> projectConfigRequirements = state.getSubmitRequirements();
+
+ ImmutableMap<String, SubmitRequirement> requirements =
+ Stream.concat(
+ globalRequirements.entrySet().stream(),
+ projectConfigRequirements.entrySet().stream())
+ .collect(
+ toImmutableMap(
+ Map.Entry::getKey,
+ Map.Entry::getValue,
+ (globalSubmitRequirement, projectConfigRequirement) ->
+ // Override with projectConfigRequirement if allowed by
+ // globalSubmitRequirement configuration
+ globalSubmitRequirement.allowOverrideInChildProjects()
+ ? projectConfigRequirement
+ : globalSubmitRequirement));
+ Map<SubmitRequirement, SubmitRequirementResult> results = new HashMap<>();
for (SubmitRequirement requirement : requirements.values()) {
- result.put(requirement, evaluateRequirement(requirement, cd));
+ results.put(requirement, evaluateRequirement(requirement, cd));
}
- return result;
+ return results;
+ }
+
+ /**
+ * Returns a map of all global {@link SubmitRequirement}s, keyed by their lower-case name.
+ *
+ * <p>The global {@link SubmitRequirement}s apply to all projects and can be bound by plugins.
+ */
+ private Map<String, SubmitRequirement> getGlobalRequirements() {
+ return globalSubmitRequirements.stream()
+ .collect(
+ toImmutableMap(
+ globalRequirement -> globalRequirement.name().toLowerCase(), Function.identity()));
}
/** Evaluate the predicate recursively using change data. */
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java b/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java
index 102d3f2..e34ab1d 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java
@@ -14,8 +14,16 @@
package com.google.gerrit.server.project;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementResult;
+import com.google.gerrit.metrics.Counter2;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
@@ -24,9 +32,53 @@
* A utility class for different operations related to {@link
* com.google.gerrit.entities.SubmitRequirement}s.
*/
+@Singleton
public class SubmitRequirementsUtil {
- private SubmitRequirementsUtil() {}
+ @Singleton
+ static class Metrics {
+ final Counter2<String, String> submitRequirementsMatchingWithLegacy;
+ final Counter2<String, String> submitRequirementsMismatchingWithLegacy;
+
+ @Inject
+ Metrics(MetricMaker metricMaker) {
+ submitRequirementsMatchingWithLegacy =
+ metricMaker.newCounter(
+ "change/submit_requirements/matching_with_legacy",
+ new Description(
+ "Total number of times there was a legacy and non-legacy "
+ + "submit requirements with the same name for a change, "
+ + "and the evaluation of both requirements had the same result "
+ + "w.r.t. change submittability.")
+ .setRate()
+ .setUnit("count"),
+ Field.ofString("project", Metadata.Builder::projectName).build(),
+ Field.ofString("sr_name", Metadata.Builder::submitRequirementName)
+ .description("Submit requirement name")
+ .build());
+ submitRequirementsMismatchingWithLegacy =
+ metricMaker.newCounter(
+ "change/submit_requirements/mismatching_with_legacy",
+ new Description(
+ "Total number of times there was a legacy and non-legacy "
+ + "submit requirements with the same name for a change, "
+ + "and the evaluation of both requirements had a different result "
+ + "w.r.t. change submittability.")
+ .setRate()
+ .setUnit("count"),
+ Field.ofString("project", Metadata.Builder::projectName).build(),
+ Field.ofString("sr_name", Metadata.Builder::submitRequirementName)
+ .description("Submit requirement name")
+ .build());
+ }
+ }
+
+ private final Metrics metrics;
+
+ @Inject
+ public SubmitRequirementsUtil(Metrics metrics) {
+ this.metrics = metrics;
+ }
/**
* Merge legacy and non-legacy submit requirement results. If both input maps have submit
@@ -43,9 +95,10 @@
* @return a map that is the result of merging both input maps, while eliminating requirements
* with the same name and status.
*/
- public static Map<SubmitRequirement, SubmitRequirementResult> mergeLegacyAndNonLegacyRequirements(
+ public Map<SubmitRequirement, SubmitRequirementResult> mergeLegacyAndNonLegacyRequirements(
Map<SubmitRequirement, SubmitRequirementResult> projectConfigRequirements,
- Map<SubmitRequirement, SubmitRequirementResult> legacyRequirements) {
+ Map<SubmitRequirement, SubmitRequirementResult> legacyRequirements,
+ Project.NameKey project) {
Map<SubmitRequirement, SubmitRequirementResult> result = new HashMap<>();
result.putAll(projectConfigRequirements);
Map<String, SubmitRequirementResult> requirementsByName =
@@ -53,12 +106,14 @@
.collect(Collectors.toMap(sr -> sr.getKey().name().toLowerCase(), sr -> sr.getValue()));
for (Map.Entry<SubmitRequirement, SubmitRequirementResult> legacy :
legacyRequirements.entrySet()) {
- String name = legacy.getKey().name().toLowerCase();
- SubmitRequirementResult projectConfigResult = requirementsByName.get(name);
+ String srName = legacy.getKey().name().toLowerCase();
+ SubmitRequirementResult projectConfigResult = requirementsByName.get(srName);
SubmitRequirementResult legacyResult = legacy.getValue();
if (projectConfigResult != null && matchByStatus(projectConfigResult, legacyResult)) {
+ metrics.submitRequirementsMatchingWithLegacy.increment(project.get(), srName);
continue;
}
+ metrics.submitRequirementsMismatchingWithLegacy.increment(project.get(), srName);
result.put(legacy.getKey(), legacy.getValue());
}
return result;
diff --git a/java/com/google/gerrit/server/project/TagResource.java b/java/com/google/gerrit/server/project/TagResource.java
index 08ef669..c837557 100644
--- a/java/com/google/gerrit/server/project/TagResource.java
+++ b/java/com/google/gerrit/server/project/TagResource.java
@@ -20,8 +20,7 @@
import com.google.inject.TypeLiteral;
public class TagResource extends RefResource {
- public static final TypeLiteral<RestView<TagResource>> TAG_KIND =
- new TypeLiteral<RestView<TagResource>>() {};
+ public static final TypeLiteral<RestView<TagResource>> TAG_KIND = new TypeLiteral<>() {};
private final TagInfo tagInfo;
diff --git a/java/com/google/gerrit/server/project/testing/TestLabels.java b/java/com/google/gerrit/server/project/testing/TestLabels.java
index 62f8560..d7bbe46 100644
--- a/java/com/google/gerrit/server/project/testing/TestLabels.java
+++ b/java/com/google/gerrit/server/project/testing/TestLabels.java
@@ -19,11 +19,16 @@
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelValue;
import java.util.Arrays;
+import java.util.Optional;
public class TestLabels {
+ public static final String CODE_REVIEW_LABEL_DESCRIPTION = "Code review label description";
+ public static final String VERIFIED_LABEL_DESCRIPTION = "Verified label description";
+
public static LabelType codeReview() {
return label(
LabelId.CODE_REVIEW,
+ CODE_REVIEW_LABEL_DESCRIPTION,
value(2, "Looks good to me, approved"),
value(1, "Looks good to me, but someone else must approve"),
value(0, "No score"),
@@ -33,7 +38,11 @@
public static LabelType verified() {
return label(
- LabelId.VERIFIED, value(1, LabelId.VERIFIED), value(0, "No score"), value(-1, "Fails"));
+ LabelId.VERIFIED,
+ VERIFIED_LABEL_DESCRIPTION,
+ value(1, LabelId.VERIFIED),
+ value(0, "No score"),
+ value(-1, "Fails"));
}
public static LabelType patchSetLock() {
@@ -48,6 +57,10 @@
return LabelValue.create((short) value, text);
}
+ public static LabelType label(String name, String description, LabelValue... values) {
+ return labelBuilder(name, values).setDescription(Optional.of(description)).build();
+ }
+
public static LabelType label(String name, LabelValue... values) {
return labelBuilder(name, values).build();
}
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index 4a95b2e..6ab51c5 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -127,9 +127,7 @@
.change(changeNotes.get())
.check(ChangePermission.READ);
} catch (AuthException e) {
- String msg = String.format("change %s not found", change);
- logger.atSevere().withCause(e).log(msg);
- throw error(msg);
+ throw error(String.format("change %s not found", change), e);
}
return AccountPredicates.cansee(args, changeNotes.get());
diff --git a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
index 1d67009..a0a9f71 100644
--- a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
+++ b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
@@ -82,7 +82,7 @@
Joiner.on(", ")
.appendTo(
msg, accountStates.stream().map(a -> a.account().id().toString()).collect(toList()));
- logger.atWarning().log(msg.toString());
+ logger.atWarning().log("%s", msg);
}
return null;
}
diff --git a/java/com/google/gerrit/server/query/approval/ApprovalContext.java b/java/com/google/gerrit/server/query/approval/ApprovalContext.java
index 4dedbb5..2839eb7 100644
--- a/java/com/google/gerrit/server/query/approval/ApprovalContext.java
+++ b/java/com/google/gerrit/server/query/approval/ApprovalContext.java
@@ -21,6 +21,8 @@
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.extensions.client.ChangeKind;
import com.google.gerrit.server.notedb.ChangeNotes;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.revwalk.RevWalk;
/** Entity representing all required information to match predicates for copying approvals. */
@AutoValue
@@ -41,8 +43,19 @@
/** {@link ChangeKind} of the delta between the origin and target patch set. */
public abstract ChangeKind changeKind();
+ /** {@link RevWalk} of the repository for the current commit. */
+ public abstract RevWalk revWalk();
+
+ /** {@link RevWalk} of the repository for the current commit. */
+ public abstract Config repoConfig();
+
public static ApprovalContext create(
- ChangeNotes changeNotes, PatchSetApproval psa, PatchSet patchSet, ChangeKind changeKind) {
+ ChangeNotes changeNotes,
+ PatchSetApproval psa,
+ PatchSet patchSet,
+ ChangeKind changeKind,
+ RevWalk revWalk,
+ Config repoConfig) {
checkState(
psa.patchSetId().changeId().equals(patchSet.id().changeId()),
"approval and target must be the same change. got: %s, %s",
@@ -54,6 +67,7 @@
// As explained in the commit message of this change doing this check is only possible if there
// are no changes with gaps in patch set numbers. Since it's planned to fix-up old changes with
// gaps in patch set numbers, this todo is a reminder to add back the check once this is done.
- return new AutoValue_ApprovalContext(psa, patchSet, changeNotes, changeKind);
+ return new AutoValue_ApprovalContext(
+ psa, patchSet, changeNotes, changeKind, revWalk, repoConfig);
}
}
diff --git a/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java b/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
index de7dd0a..2a72c49 100644
--- a/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
+++ b/java/com/google/gerrit/server/query/approval/ListOfFilesUnchangedPredicate.java
@@ -23,7 +23,8 @@
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.filediff.FileDiffOutput;
+import com.google.gerrit.server.patch.DiffOptions;
+import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
@@ -58,17 +59,30 @@
Integer parentNum =
isInitialCommit(ctx.changeNotes().getProjectName(), targetPatchSet.commitId()) ? 0 : 1;
try {
- Map<String, FileDiffOutput> baseVsCurrent =
- diffOperations.listModifiedFilesAgainstParent(
- ctx.changeNotes().getProjectName(), targetPatchSet.commitId(), parentNum);
- Map<String, FileDiffOutput> baseVsPrior =
- diffOperations.listModifiedFilesAgainstParent(
- ctx.changeNotes().getProjectName(), sourcePatchSet.commitId(), parentNum);
- Map<String, FileDiffOutput> priorVsCurrent =
- diffOperations.listModifiedFiles(
+ Map<String, ModifiedFile> baseVsCurrent =
+ diffOperations.loadModifiedFilesAgainstParent(
+ ctx.changeNotes().getProjectName(),
+ targetPatchSet.commitId(),
+ parentNum,
+ DiffOptions.DEFAULTS,
+ ctx.revWalk(),
+ ctx.repoConfig());
+ Map<String, ModifiedFile> baseVsPrior =
+ diffOperations.loadModifiedFilesAgainstParent(
ctx.changeNotes().getProjectName(),
sourcePatchSet.commitId(),
- targetPatchSet.commitId());
+ parentNum,
+ DiffOptions.DEFAULTS,
+ ctx.revWalk(),
+ ctx.repoConfig());
+ Map<String, ModifiedFile> priorVsCurrent =
+ diffOperations.loadModifiedFiles(
+ ctx.changeNotes().getProjectName(),
+ sourcePatchSet.commitId(),
+ targetPatchSet.commitId(),
+ DiffOptions.DEFAULTS,
+ ctx.revWalk(),
+ ctx.repoConfig());
return match(baseVsCurrent, baseVsPrior, priorVsCurrent);
} catch (DiffNotAvailableException ex) {
throw new StorageException(
@@ -84,9 +98,9 @@
* {@link ChangeType} matches for each modified file.
*/
public boolean match(
- Map<String, FileDiffOutput> baseVsCurrent,
- Map<String, FileDiffOutput> baseVsPrior,
- Map<String, FileDiffOutput> priorVsCurrent) {
+ Map<String, ModifiedFile> baseVsCurrent,
+ Map<String, ModifiedFile> baseVsPrior,
+ Map<String, ModifiedFile> priorVsCurrent) {
Set<String> allFiles = new HashSet<>();
allFiles.addAll(baseVsCurrent.keySet());
allFiles.addAll(baseVsPrior.keySet());
@@ -94,17 +108,17 @@
if (Patch.isMagic(file)) {
continue;
}
- FileDiffOutput fileDiffOutput1 = baseVsCurrent.get(file);
- FileDiffOutput fileDiffOutput2 = baseVsPrior.get(file);
+ ModifiedFile modifiedFile1 = baseVsCurrent.get(file);
+ ModifiedFile modifiedFile2 = baseVsPrior.get(file);
if (!priorVsCurrent.containsKey(file)) {
// If the file is not modified between prior and current patchsets, then scan safely skip
- // it. The file might has been modified due to rebase.
+ // it. The file might have been modified due to rebase.
continue;
}
- if (fileDiffOutput1 == null || fileDiffOutput2 == null) {
+ if (modifiedFile1 == null || modifiedFile2 == null) {
return false;
}
- if (!fileDiffOutput2.changeType().equals(fileDiffOutput1.changeType())) {
+ if (!modifiedFile2.changeType().equals(modifiedFile1.changeType())) {
return false;
}
}
diff --git a/java/com/google/gerrit/server/query/change/AfterPredicate.java b/java/com/google/gerrit/server/query/change/AfterPredicate.java
index 8f92d9a..2514989 100644
--- a/java/com/google/gerrit/server/query/change/AfterPredicate.java
+++ b/java/com/google/gerrit/server/query/change/AfterPredicate.java
@@ -17,14 +17,14 @@
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.query.QueryParseException;
import java.sql.Timestamp;
-import java.util.Date;
+import java.time.Instant;
/**
* Predicate that matches a {@link Timestamp} field from the index in a range from the passed {@code
* String} representation of the Timestamp value to the maximum supported time.
*/
public class AfterPredicate extends TimestampRangeChangePredicate {
- protected final Date cut;
+ protected final Instant cut;
public AfterPredicate(FieldDef<ChangeData, Timestamp> def, String name, String value)
throws QueryParseException {
@@ -33,13 +33,13 @@
}
@Override
- public Date getMinTimestamp() {
+ public Instant getMinTimestamp() {
return cut;
}
@Override
- public Date getMaxTimestamp() {
- return new Date(Long.MAX_VALUE);
+ public Instant getMaxTimestamp() {
+ return Instant.ofEpochMilli(Long.MAX_VALUE);
}
@Override
@@ -48,6 +48,6 @@
if (valueTimestamp == null) {
return false;
}
- return valueTimestamp.getTime() >= cut.getTime();
+ return valueTimestamp.getTime() >= cut.toEpochMilli();
}
}
diff --git a/java/com/google/gerrit/server/query/change/AgePredicate.java b/java/com/google/gerrit/server/query/change/AgePredicate.java
index d38789f..c1138bd 100644
--- a/java/com/google/gerrit/server/query/change/AgePredicate.java
+++ b/java/com/google/gerrit/server/query/change/AgePredicate.java
@@ -21,26 +21,27 @@
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.util.time.TimeUtil;
import java.sql.Timestamp;
+import java.time.Instant;
public class AgePredicate extends TimestampRangeChangePredicate {
- protected final long cut;
+ protected final Instant cut;
public AgePredicate(String value) {
super(ChangeField.UPDATED, ChangeQueryBuilder.FIELD_AGE, value);
long s = ConfigUtil.getTimeUnit(getValue(), 0, SECONDS);
long ms = MILLISECONDS.convert(s, SECONDS);
- this.cut = TimeUtil.nowMs() - ms;
+ this.cut = Instant.ofEpochMilli(TimeUtil.nowMs() - ms);
}
@Override
- public Timestamp getMinTimestamp() {
- return new Timestamp(0);
+ public Instant getMinTimestamp() {
+ return Instant.EPOCH;
}
@Override
- public Timestamp getMaxTimestamp() {
- return new Timestamp(cut);
+ public Instant getMaxTimestamp() {
+ return cut;
}
@Override
@@ -49,6 +50,6 @@
if (valueTimestamp == null) {
return false;
}
- return valueTimestamp.getTime() <= cut;
+ return valueTimestamp.getTime() <= cut.toEpochMilli();
}
}
diff --git a/java/com/google/gerrit/server/query/change/BeforePredicate.java b/java/com/google/gerrit/server/query/change/BeforePredicate.java
index 6e28ce6..5d682fb 100644
--- a/java/com/google/gerrit/server/query/change/BeforePredicate.java
+++ b/java/com/google/gerrit/server/query/change/BeforePredicate.java
@@ -17,14 +17,14 @@
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.query.QueryParseException;
import java.sql.Timestamp;
-import java.util.Date;
+import java.time.Instant;
/**
* Predicate that matches a {@link Timestamp} field from the index in a range from the the epoch to
* the passed {@code String} representation of the Timestamp value.
*/
public class BeforePredicate extends TimestampRangeChangePredicate {
- protected final Date cut;
+ protected final Instant cut;
public BeforePredicate(FieldDef<ChangeData, Timestamp> def, String name, String value)
throws QueryParseException {
@@ -33,12 +33,12 @@
}
@Override
- public Date getMinTimestamp() {
- return new Date(0);
+ public Instant getMinTimestamp() {
+ return Instant.EPOCH;
}
@Override
- public Date getMaxTimestamp() {
+ public Instant getMaxTimestamp() {
return cut;
}
@@ -48,6 +48,6 @@
if (valueTimestamp == null) {
return false;
}
- return valueTimestamp.getTime() <= cut.getTime();
+ return valueTimestamp.getTime() <= cut.toEpochMilli();
}
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 9961519..94dad84 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -75,8 +75,6 @@
import com.google.gerrit.server.change.PureRevert;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.TrackingFooters;
-import com.google.gerrit.server.experiments.ExperimentFeatures;
-import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -98,7 +96,7 @@
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -278,7 +276,7 @@
.id(PatchSet.id(id, currentPatchSetId))
.commitId(commitId)
.uploader(Account.id(1000))
- .createdOn(TimeUtil.nowTs())
+ .createdOn(TimeUtil.now())
.build();
return cd;
}
@@ -290,7 +288,6 @@
private final ChangeMessagesUtil cmUtil;
private final ChangeNotes.Factory notesFactory;
private final CommentsUtil commentsUtil;
- private final ExperimentFeatures experimentFeatures;
private final GitRepositoryManager repoManager;
private final MergeUtil.Factory mergeUtilFactory;
private final MergeabilityCache mergeabilityCache;
@@ -300,6 +297,7 @@
private final TrackingFooters trackingFooters;
private final PureRevert pureRevert;
private final SubmitRequirementsEvaluator submitRequirementsEvaluator;
+ private final SubmitRequirementsUtil submitRequirementsUtil;
private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
// Required assisted injected fields.
@@ -360,7 +358,7 @@
private Integer unresolvedCommentCount;
private Integer totalCommentCount;
private LabelTypes labelTypes;
- private Optional<Timestamp> mergedOn;
+ private Optional<Instant> mergedOn;
private ImmutableSetMultimap<NameKey, RefState> refStates;
private ImmutableList<byte[]> refStatePatterns;
@@ -372,7 +370,6 @@
ChangeMessagesUtil cmUtil,
ChangeNotes.Factory notesFactory,
CommentsUtil commentsUtil,
- ExperimentFeatures experimentFeatures,
GitRepositoryManager repoManager,
MergeUtil.Factory mergeUtilFactory,
MergeabilityCache mergeabilityCache,
@@ -382,6 +379,7 @@
TrackingFooters trackingFooters,
PureRevert pureRevert,
SubmitRequirementsEvaluator submitRequirementsEvaluator,
+ SubmitRequirementsUtil submitRequirementsUtil,
SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory,
@Assisted Project.NameKey project,
@Assisted Change.Id id,
@@ -392,7 +390,6 @@
this.cmUtil = cmUtil;
this.notesFactory = notesFactory;
this.commentsUtil = commentsUtil;
- this.experimentFeatures = experimentFeatures;
this.repoManager = repoManager;
this.mergeUtilFactory = mergeUtilFactory;
this.mergeabilityCache = mergeabilityCache;
@@ -403,6 +400,7 @@
this.trackingFooters = trackingFooters;
this.pureRevert = pureRevert;
this.submitRequirementsEvaluator = submitRequirementsEvaluator;
+ this.submitRequirementsUtil = submitRequirementsUtil;
this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
this.project = project;
@@ -424,6 +422,10 @@
return this;
}
+ public StorageConstraint getStorageConstraint() {
+ return storageConstraint;
+ }
+
/** Returns {@code true} if we allow reading data from NoteDb. */
public boolean lazyload() {
return storageConstraint.ordinal()
@@ -707,7 +709,7 @@
* @throws StorageException if {@code lazyLoad} is off, {@link ChangeNotes} can not be loaded
* because we do not expect to call the database.
*/
- public Optional<Timestamp> getMergedOn() throws StorageException {
+ public Optional<Instant> getMergedOn() throws StorageException {
if (mergedOn == null) {
// The value was not loaded yet, try to get from the database.
mergedOn = notes().getMergedOn();
@@ -716,7 +718,7 @@
}
/** Sets the value e.g. when loading from index. */
- public void setMergedOn(@Nullable Timestamp mergedOn) {
+ public void setMergedOn(@Nullable Instant mergedOn) {
this.mergedOn = Optional.ofNullable(mergedOn);
}
@@ -770,7 +772,7 @@
if (!lazyload()) {
return ImmutableListMultimap.of();
}
- allApprovals = approvalsUtil.byChange(notes());
+ allApprovals = approvalsUtil.byChangeExcludingCopiedApprovals(notes());
}
return allApprovals;
}
@@ -948,10 +950,6 @@
* com.google.gerrit.server.index.change.ChangeField#STORED_SUBMIT_REQUIREMENTS}.
*/
public Map<SubmitRequirement, SubmitRequirementResult> submitRequirements() {
- if (!experimentFeatures.isFeatureEnabled(
- ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS)) {
- return Collections.emptyMap();
- }
if (submitRequirements == null) {
if (!lazyload()) {
return Collections.emptyMap();
@@ -969,25 +967,17 @@
.filter(r -> !r.isLegacy())
.collect(Collectors.toMap(r -> r.submitRequirement(), Function.identity()));
Map<SubmitRequirement, SubmitRequirementResult> legacyRequirements =
- SubmitRequirementsAdapter.getLegacyRequirements(submitRuleEvaluatorFactory, this);
+ SubmitRequirementsAdapter.getLegacyRequirements(this);
submitRequirements =
- SubmitRequirementsUtil.mergeLegacyAndNonLegacyRequirements(
- projectConfigRequirements, legacyRequirements);
+ submitRequirementsUtil.mergeLegacyAndNonLegacyRequirements(
+ projectConfigRequirements, legacyRequirements, project());
}
return submitRequirements;
}
public void setSubmitRequirements(
Map<SubmitRequirement, SubmitRequirementResult> submitRequirements) {
- if (!experimentFeatures.isFeatureEnabled(
- ExperimentFeaturesConstants
- .GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS_BACKFILLING_ON_DASHBOARD)) {
- // Only set back values from the index if the experiment is not active. While the experiment
- // is active, we want
- // to compute SRs from scratch to ensure fresh results.
- // TODO(ghareeb, hiesel): Remove this.
- this.submitRequirements = submitRequirements;
- }
+ this.submitRequirements = submitRequirements;
}
public List<SubmitRecord> submitRecords(SubmitRuleOptions options) {
@@ -1348,7 +1338,7 @@
public abstract Account.Id author();
- public abstract Timestamp ts();
+ public abstract Instant ts();
}
@AutoValue
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
index 043b37d..355f9de 100644
--- a/java/com/google/gerrit/server/query/change/ChangePredicates.java
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.query.change;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
import com.google.common.base.CharMatcher;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
@@ -21,12 +23,15 @@
import com.google.gerrit.entities.Project;
import com.google.gerrit.git.ObjectIds;
import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.change.HashtagsUtil;
import com.google.gerrit.server.index.change.ChangeField;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
+import java.util.Set;
/** Predicates that match against {@link ChangeData}. */
public class ChangePredicates {
@@ -76,8 +81,37 @@
* Returns a predicate that matches changes where the provided {@link
* com.google.gerrit.entities.Account.Id} has a pending draft comment.
*/
- public static Predicate<ChangeData> draftBy(Account.Id id) {
- return new ChangeIndexPredicate(ChangeField.DRAFTBY, id.toString());
+ public static Predicate<ChangeData> draftBy(
+ boolean computeFromAllUsersRepository, CommentsUtil commentsUtil, Account.Id id) {
+ if (!computeFromAllUsersRepository) {
+ return new ChangeIndexPredicate(ChangeField.DRAFTBY, id.toString());
+ }
+ Set<Predicate<ChangeData>> changeIdPredicates =
+ commentsUtil.getChangesWithDrafts(id).stream()
+ .map(ChangePredicates::idStr)
+ .collect(toImmutableSet());
+ return changeIdPredicates.isEmpty()
+ ? ChangeIndexPredicate.none()
+ : Predicate.or(changeIdPredicates);
+ }
+
+ /**
+ * Returns a predicate that matches changes where the provided {@link
+ * com.google.gerrit.entities.Account.Id} has starred changes with {@code label}.
+ */
+ public static Predicate<ChangeData> starBy(
+ boolean computeFromAllUsersRepository,
+ StarredChangesUtil starredChangesUtil,
+ Account.Id id,
+ String label) {
+ if (!computeFromAllUsersRepository) {
+ return new StarPredicate(id, label);
+ }
+ Set<Predicate<ChangeData>> starredChanges =
+ starredChangesUtil.byAccountId(id, label).stream()
+ .map(ChangePredicates::idStr)
+ .collect(toImmutableSet());
+ return starredChanges.isEmpty() ? ChangeIndexPredicate.none() : Predicate.or(starredChanges);
}
/**
@@ -173,6 +207,11 @@
return new ChangeIndexPredicate(ChangeField.FUZZY_TOPIC, topic);
}
+ /** Returns a predicate that matches changes in the provided {@code topic}. Used with prefixes */
+ public static Predicate<ChangeData> prefixTopic(String topic) {
+ return new ChangeIndexPredicate(ChangeField.PREFIX_TOPIC, topic);
+ }
+
/** 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);
@@ -197,6 +236,15 @@
ChangeField.FUZZY_HASHTAG, HashtagsUtil.cleanupHashtag(hashtag).toLowerCase());
}
+ /**
+ * Returns a predicate that matches changes in the provided {@code hashtag}. Used with prefixes
+ */
+ public static Predicate<ChangeData> prefixHashtag(String hashtag) {
+ // Use toLowerCase without locale to match behavior in ChangeField.
+ return new ChangeIndexPredicate(
+ ChangeField.PREFIX_HASHTAG, HashtagsUtil.cleanupHashtag(hashtag).toLowerCase());
+ }
+
/** 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);
@@ -311,4 +359,23 @@
public static Predicate<ChangeData> submitRuleStatus(String value) {
return new ChangeIndexPredicate(ChangeField.SUBMIT_RULE_RESULT, value);
}
+
+ /**
+ * Returns a predicate that matches with changes that are pure reverts if {@code value} is equal
+ * 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);
+ }
+
+ /**
+ * Returns a predicate that matches with changes that are submittable if {@code value} is equal to
+ * "1", or non-submittable if {@code value} is "0".
+ *
+ * <p>The computation of this field is based on the evaluation of {@link
+ * com.google.gerrit.entities.SubmitRequirement}s.
+ */
+ public static Predicate<ChangeData> isSubmittable(String value) {
+ return new ChangeIndexPredicate(ChangeField.IS_SUBMITTABLE, value);
+ }
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index da36633..e5f6d32 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -17,6 +17,7 @@
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.gerrit.entities.Change.CHANGE_ID_PATTERN;
import static com.google.gerrit.server.account.AccountResolver.isSelf;
+import static com.google.gerrit.server.experiments.ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY;
import static com.google.gerrit.server.query.change.ChangeData.asChanges;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
@@ -42,6 +43,7 @@
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.SchemaUtil;
@@ -70,6 +72,7 @@
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.HasOperandAliasConfig;
import com.google.gerrit.server.config.OperatorAliasConfig;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.index.change.ChangeIndex;
@@ -81,6 +84,7 @@
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.project.ChildProjects;
import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.query.change.PredicateArgs.ValOp;
import com.google.gerrit.server.rules.SubmitRule;
import com.google.gerrit.server.submit.SubmitDryRun;
import com.google.inject.Inject;
@@ -137,7 +141,7 @@
static final int MAX_ACCOUNTS_PER_DEFAULT_FIELD = 10;
// NOTE: As new search operations are added, please keep the suggestions in
- // gr-search-bar.js up to date.
+ // gr-search-bar.ts up to date.
public static final String FIELD_ADDED = "added";
public static final String FIELD_AGE = "age";
@@ -201,15 +205,18 @@
public static final String FIELD_WATCHEDBY = "watchedby";
public static final String FIELD_WIP = "wip";
public static final String FIELD_REVERTOF = "revertof";
+ public static final String FIELD_PURE_REVERT = "ispurerevert";
public static final String FIELD_CHERRYPICK = "cherrypick";
public static final String FIELD_CHERRY_PICK_OF_CHANGE = "cherrypickofchange";
public static final String FIELD_CHERRY_PICK_OF_PATCHSET = "cherrypickofpatchset";
+ public static final String FIELD_IS_SUBMITTABLE = "issubmittable";
public static final String ARG_ID_NAME = "name";
public static final String ARG_ID_USER = "user";
public static final String ARG_ID_GROUP = "group";
public static final String ARG_ID_OWNER = "owner";
public static final String ARG_ID_NON_UPLOADER = "non_uploader";
+ public static final String ARG_COUNT = "count";
public static final Account.Id OWNER_ACCOUNT_ID = Account.id(0);
public static final Account.Id NON_UPLOADER_ACCOUNT_ID = Account.id(-1);
@@ -253,6 +260,7 @@
final OperatorAliasConfig operatorAliasConfig;
final boolean indexMergeable;
final boolean conflictsPredicateEnabled;
+ final ExperimentFeatures experimentFeatures;
final HasOperandAliasConfig hasOperandAliasConfig;
final PluginSetContext<SubmitRule> submitRules;
@@ -288,6 +296,7 @@
GroupMembers groupMembers,
OperatorAliasConfig operatorAliasConfig,
@GerritServerConfig Config gerritConfig,
+ ExperimentFeatures experimentFeatures,
HasOperandAliasConfig hasOperandAliasConfig,
ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory,
PluginSetContext<SubmitRule> submitRules) {
@@ -320,6 +329,7 @@
operatorAliasConfig,
MergeabilityComputationBehavior.fromConfig(gerritConfig).includeInIndex(),
gerritConfig.getBoolean("change", null, "conflictsPredicateEnabled", true),
+ experimentFeatures,
hasOperandAliasConfig,
changeIsVisbleToPredicateFactory,
submitRules);
@@ -354,6 +364,7 @@
OperatorAliasConfig operatorAliasConfig,
boolean indexMergeable,
boolean conflictsPredicateEnabled,
+ ExperimentFeatures experimentFeatures,
HasOperandAliasConfig hasOperandAliasConfig,
ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory,
PluginSetContext<SubmitRule> submitRules) {
@@ -386,6 +397,7 @@
this.operatorAliasConfig = operatorAliasConfig;
this.indexMergeable = indexMergeable;
this.conflictsPredicateEnabled = conflictsPredicateEnabled;
+ this.experimentFeatures = experimentFeatures;
this.hasOperandAliasConfig = hasOperandAliasConfig;
this.submitRules = submitRules;
}
@@ -420,6 +432,7 @@
operatorAliasConfig,
indexMergeable,
conflictsPredicateEnabled,
+ experimentFeatures,
hasOperandAliasConfig,
changeIsVisbleToPredicateFactory,
submitRules);
@@ -465,6 +478,10 @@
private final Arguments args;
protected Map<String, String> hasOperandAliases = Collections.emptyMap();
+ private static final Splitter RULE_SPLITTER = Splitter.on("=");
+ private static final Splitter PLUGIN_SPLITTER = Splitter.on("_");
+ private static final Splitter LABEL_SPLITTER = Splitter.on(",");
+
@Inject
ChangeQueryBuilder(Arguments args) {
this(mydef, args);
@@ -513,22 +530,14 @@
@Operator
public Predicate<ChangeData> mergedBefore(String value) throws QueryParseException {
- if (!args.index.getSchema().hasField(ChangeField.MERGED_ON)) {
- throw new QueryParseException(
- String.format(
- "'%s' operator is not supported by change index version", OPERATOR_MERGED_BEFORE));
- }
+ checkFieldAvailable(ChangeField.MERGED_ON, OPERATOR_MERGED_BEFORE);
return new BeforePredicate(
ChangeField.MERGED_ON, ChangeQueryBuilder.OPERATOR_MERGED_BEFORE, value);
}
@Operator
public Predicate<ChangeData> mergedAfter(String value) throws QueryParseException {
- if (!args.index.getSchema().hasField(ChangeField.MERGED_ON)) {
- throw new QueryParseException(
- String.format(
- "'%s' operator is not supported by change index version", OPERATOR_MERGED_AFTER));
- }
+ checkFieldAvailable(ChangeField.MERGED_ON, OPERATOR_MERGED_AFTER);
return new AfterPredicate(
ChangeField.MERGED_ON, ChangeQueryBuilder.OPERATOR_MERGED_AFTER, value);
}
@@ -577,16 +586,16 @@
public Predicate<ChangeData> rule(String value) throws QueryParseException {
String ruleNameArg = value;
String statusArg = null;
- String[] queryArgs = value.split("=");
- if (queryArgs.length > 2) {
+ List<String> queryArgs = RULE_SPLITTER.splitToList(value);
+ if (queryArgs.size() > 2) {
throw new QueryParseException(
"Invalid query arguments. Correct format is 'rule:<rule_name>=<status>' "
+ "with <rule_name> in the form of <plugin>~<rule>. For Gerrit core rules, "
+ "rule name should be specified either as gerrit~<rule> or <rule>.");
}
- if (queryArgs.length == 2) {
- ruleNameArg = queryArgs[0];
- statusArg = queryArgs[1];
+ if (queryArgs.size() == 2) {
+ ruleNameArg = queryArgs.get(0);
+ statusArg = queryArgs.get(1);
}
// If ruleName is not prefixed by the plugin name, add the "gerrit~" prefix to it.
@@ -618,10 +627,7 @@
}
if ("attention".equalsIgnoreCase(value)) {
- if (!args.index.getSchema().hasField(ChangeField.ATTENTION_SET_USERS)) {
- throw new QueryParseException(
- "'has:attention' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "has:attention");
return new IsAttentionPredicate();
}
@@ -630,7 +636,7 @@
}
// for plugins the value will be operandName_pluginName
- List<String> names = Lists.newArrayList(Splitter.on('_').split(value));
+ List<String> names = PLUGIN_SPLITTER.splitToList(value);
if (names.size() == 2) {
ChangeHasOperandFactory op = args.hasOperands.get(names.get(1), names.get(0));
if (op != null) {
@@ -664,20 +670,13 @@
}
if ("uploader".equalsIgnoreCase(value)) {
- if (!args.getSchema().hasField(ChangeField.UPLOADER)) {
- throw new QueryParseException(
- "'is:uploader' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.UPLOADER, "is:uploader");
return ChangePredicates.uploader(self());
}
if ("reviewer".equalsIgnoreCase(value)) {
- if (args.getSchema().hasField(ChangeField.WIP)) {
- return Predicate.and(
- Predicate.not(new BooleanPredicate(ChangeField.WIP)),
- ReviewerPredicate.reviewer(self()));
- }
- return ReviewerPredicate.reviewer(self());
+ return Predicate.and(
+ Predicate.not(new BooleanPredicate(ChangeField.WIP)), ReviewerPredicate.reviewer(self()));
}
if ("cc".equalsIgnoreCase(value)) {
@@ -692,25 +691,16 @@
}
if ("merge".equalsIgnoreCase(value)) {
- if (args.getSchema().hasField(ChangeField.MERGE)) {
- return new BooleanPredicate(ChangeField.MERGE);
- }
- throw new QueryParseException("'is:merge' operator is not supported by change index version");
+ checkFieldAvailable(ChangeField.MERGE, "is:merge");
+ return new BooleanPredicate(ChangeField.MERGE);
}
if ("private".equalsIgnoreCase(value)) {
- if (args.getSchema().hasField(ChangeField.PRIVATE)) {
- return new BooleanPredicate(ChangeField.PRIVATE);
- }
- throw new QueryParseException(
- "'is:private' operator is not supported by change index version");
+ return new BooleanPredicate(ChangeField.PRIVATE);
}
if ("attention".equalsIgnoreCase(value)) {
- if (!args.index.getSchema().hasField(ChangeField.ATTENTION_SET_USERS)) {
- throw new QueryParseException(
- "'is:attention' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "is:attention");
return new IsAttentionPredicate();
}
@@ -722,16 +712,25 @@
return ChangePredicates.assignee(Account.id(ChangeField.NO_ASSIGNEE));
}
+ if ("pure-revert".equalsIgnoreCase(value)) {
+ checkFieldAvailable(ChangeField.IS_PURE_REVERT, "is:pure-revert");
+ return ChangePredicates.pureRevert("1");
+ }
+
if ("submittable".equalsIgnoreCase(value)) {
- // 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
- // more negative cases for CLOSED and FORCED, instead make sure at least
- // one submit record is OK.
- return Predicate.and(
- new SubmittablePredicate(SubmitRecord.Status.OK),
- Predicate.not(new SubmittablePredicate(SubmitRecord.Status.NOT_READY)),
- Predicate.not(new SubmittablePredicate(SubmitRecord.Status.RULE_ERROR)));
+ if (!args.index.getSchema().hasField(ChangeField.IS_SUBMITTABLE)) {
+ // 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
+ // more negative cases for CLOSED and FORCED, instead make sure at least
+ // one submit record is OK.
+ return Predicate.and(
+ new SubmittablePredicate(SubmitRecord.Status.OK),
+ Predicate.not(new SubmittablePredicate(SubmitRecord.Status.NOT_READY)),
+ Predicate.not(new SubmittablePredicate(SubmitRecord.Status.RULE_ERROR)));
+ }
+ checkFieldAvailable(ChangeField.IS_SUBMITTABLE, "is:submittable");
+ return new IsSubmittablePredicate();
}
if ("ignored".equalsIgnoreCase(value)) {
@@ -739,30 +738,21 @@
}
if ("started".equalsIgnoreCase(value)) {
- if (args.getSchema().hasField(ChangeField.STARTED)) {
- return new BooleanPredicate(ChangeField.STARTED);
- }
- throw new QueryParseException(
- "'is:started' operator is not supported by change index version");
+ checkFieldAvailable(ChangeField.STARTED, "is:started");
+ return new BooleanPredicate(ChangeField.STARTED);
}
if ("wip".equalsIgnoreCase(value)) {
- if (args.getSchema().hasField(ChangeField.WIP)) {
- return new BooleanPredicate(ChangeField.WIP);
- }
- throw new QueryParseException("'is:wip' operator is not supported by change index version");
+ return new BooleanPredicate(ChangeField.WIP);
}
if ("cherrypick".equalsIgnoreCase(value)) {
- if (args.getSchema().hasField(ChangeField.CHERRY_PICK)) {
- return new BooleanPredicate(ChangeField.CHERRY_PICK);
- }
- throw new QueryParseException(
- "'is:cherrypick' operator is not supported by change index version");
+ checkFieldAvailable(ChangeField.CHERRY_PICK, "is:cherrypick");
+ return new BooleanPredicate(ChangeField.CHERRY_PICK);
}
// for plugins the value will be operandName_pluginName
- List<String> names = Lists.newArrayList(Splitter.on('_').split(value));
+ List<String> names = PLUGIN_SPLITTER.splitToList(value);
if (names.size() == 2) {
ChangeIsOperandFactory op = args.isOperands.get(names.get(1), names.get(0));
if (op != null) {
@@ -875,14 +865,21 @@
return ChangePredicates.hashtag(hashtag);
}
- if (!args.index.getSchema().hasField(ChangeField.FUZZY_HASHTAG)) {
- throw new QueryParseException(
- "'inhashtag' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.FUZZY_HASHTAG, "inhashtag");
return ChangePredicates.fuzzyHashtag(hashtag);
}
@Operator
+ public Predicate<ChangeData> prefixhashtag(String hashtag) throws QueryParseException {
+ if (hashtag.isEmpty()) {
+ return ChangePredicates.hashtag(hashtag);
+ }
+
+ checkFieldAvailable(ChangeField.PREFIX_HASHTAG, "prefixhashtag");
+ return ChangePredicates.prefixHashtag(hashtag);
+ }
+
+ @Operator
public Predicate<ChangeData> topic(String name) {
return ChangePredicates.exactTopic(name);
}
@@ -899,6 +896,16 @@
}
@Operator
+ public Predicate<ChangeData> prefixtopic(String name) throws QueryParseException {
+ if (name.isEmpty()) {
+ return ChangePredicates.exactTopic(name);
+ }
+
+ checkFieldAvailable(ChangeField.PREFIX_TOPIC, "prefixtopic");
+ return ChangePredicates.prefixTopic(name);
+ }
+
+ @Operator
public Predicate<ChangeData> ref(String ref) throws QueryParseException {
if (ref.startsWith("^")) {
return new RegexRefPredicate(ref);
@@ -928,54 +935,41 @@
}
@Operator
- public Predicate<ChangeData> ext(String ext) throws QueryParseException {
+ public Predicate<ChangeData> ext(String ext) {
return extension(ext);
}
@Operator
- public Predicate<ChangeData> extension(String ext) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.EXTENSION)) {
- return new FileExtensionPredicate(ext);
- }
- throw new QueryParseException("'extension' operator is not supported by change index version");
+ public Predicate<ChangeData> extension(String ext) {
+ return new FileExtensionPredicate(ext);
}
@Operator
- public Predicate<ChangeData> onlyexts(String extList) throws QueryParseException {
+ public Predicate<ChangeData> onlyexts(String extList) {
return onlyextensions(extList);
}
@Operator
- public Predicate<ChangeData> onlyextensions(String extList) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.ONLY_EXTENSIONS)) {
- return new FileExtensionListPredicate(extList);
- }
- throw new QueryParseException(
- "'onlyextensions' operator is not supported by change index version");
+ public Predicate<ChangeData> onlyextensions(String extList) {
+ return new FileExtensionListPredicate(extList);
}
@Operator
- public Predicate<ChangeData> footer(String footer) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.FOOTER)) {
- return ChangePredicates.footer(footer);
- }
- throw new QueryParseException("'footer' operator is not supported by change index version");
+ public Predicate<ChangeData> footer(String footer) {
+ return ChangePredicates.footer(footer);
}
@Operator
- public Predicate<ChangeData> dir(String directory) throws QueryParseException {
+ public Predicate<ChangeData> dir(String directory) {
return directory(directory);
}
@Operator
- public Predicate<ChangeData> directory(String directory) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.DIRECTORY)) {
- if (directory.startsWith("^")) {
- return new RegexDirectoryPredicate(directory);
- }
- return ChangePredicates.directory(directory);
+ public Predicate<ChangeData> directory(String directory) {
+ if (directory.startsWith("^")) {
+ return new RegexDirectoryPredicate(directory);
}
- throw new QueryParseException("'directory' operator is not supported by change index version");
+ return ChangePredicates.directory(directory);
}
@Operator
@@ -983,6 +977,8 @@
throws QueryParseException, IOException, ConfigInvalidException {
Set<Account.Id> accounts = null;
AccountGroup.UUID group = null;
+ Integer count = null;
+ PredicateArgs.Operator countOp = null;
// Parse for:
// label:Code-Review=1,user=jsmith or
@@ -993,24 +989,48 @@
// Special case: votes by owners can be tracked with ",owner":
// label:Code-Review+2,owner
// label:Code-Review+2,user=owner
- List<String> splitReviewer = Lists.newArrayList(Splitter.on(',').limit(2).split(name));
+ // label:Code-Review+1,count=2
+ List<String> splitReviewer = LABEL_SPLITTER.limit(2).splitToList(name);
name = splitReviewer.get(0); // remove all but the vote piece, e.g.'CodeReview=1'
if (splitReviewer.size() == 2) {
// process the user/group piece
PredicateArgs lblArgs = new PredicateArgs(splitReviewer.get(1));
- for (Map.Entry<String, String> pair : lblArgs.keyValue.entrySet()) {
- if (pair.getKey().equalsIgnoreCase(ARG_ID_USER)) {
- if (pair.getValue().equals(ARG_ID_OWNER)) {
+ // Disallow using the "count=" arg in conjunction with the "user=" or "group=" args. to avoid
+ // unnecessary complexity.
+ assertDisjunctive(lblArgs, ARG_COUNT, ARG_ID_USER);
+ assertDisjunctive(lblArgs, ARG_COUNT, ARG_ID_GROUP);
+
+ for (Map.Entry<String, ValOp> pair : lblArgs.keyValue.entrySet()) {
+ String key = pair.getKey();
+ String value = pair.getValue().value();
+ PredicateArgs.Operator operator = pair.getValue().operator();
+ if (key.equalsIgnoreCase(ARG_ID_USER)) {
+ if (value.equals(ARG_ID_OWNER)) {
accounts = Collections.singleton(OWNER_ACCOUNT_ID);
- } else if (pair.getValue().equals(ARG_ID_NON_UPLOADER)) {
+ } else if (value.equals(ARG_ID_NON_UPLOADER)) {
accounts = Collections.singleton(NON_UPLOADER_ACCOUNT_ID);
} else {
- accounts = parseAccount(pair.getValue());
+ accounts = parseAccount(value);
}
- } else if (pair.getKey().equalsIgnoreCase(ARG_ID_GROUP)) {
- group = parseGroup(pair.getValue()).getUUID();
+ } else if (key.equalsIgnoreCase(ARG_ID_GROUP)) {
+ group = parseGroup(value).getUUID();
+ } else if (key.equalsIgnoreCase(ARG_COUNT)) {
+ if (!isInt(value)) {
+ throw new QueryParseException("Invalid count argument. Value should be an integer");
+ }
+ count = Integer.parseInt(value);
+ countOp = operator;
+ if (count == 0) {
+ throw new QueryParseException("Argument count=0 is not allowed.");
+ }
+ if (count > LabelPredicate.MAX_COUNT) {
+ throw new QueryParseException(
+ String.format(
+ "count=%d is not allowed. Maximum allowed value for count is %d.",
+ count, LabelPredicate.MAX_COUNT));
+ }
} else {
throw new QueryParseException("Invalid argument identifier '" + pair.getKey() + "'");
}
@@ -1047,7 +1067,7 @@
// If the vote piece looks like Code-Review=NEED with a valid non-numeric
// submit record status, interpret as a submit record query.
int eq = name.indexOf('=');
- if (args.getSchema().hasField(ChangeField.SUBMIT_RECORD) && eq > 0) {
+ if (eq > 0) {
String statusName = name.substring(eq + 1).toUpperCase();
if (!isInt(statusName) && !MagicLabelValue.tryParse(statusName).isPresent()) {
SubmitRecord.Label.Status status =
@@ -1059,7 +1079,18 @@
}
}
- return new LabelPredicate(args, name, accounts, group);
+ return new LabelPredicate(args, name, accounts, group, count, countOp);
+ }
+
+ /** Assert that keys {@code k1} and {@code k2} do not exist in {@code labelArgs} together. */
+ private void assertDisjunctive(PredicateArgs labelArgs, String k1, String k2)
+ throws QueryParseException {
+ Map<String, ValOp> keyValArgs = labelArgs.keyValue;
+ if (keyValArgs.containsKey(k1) && keyValArgs.containsKey(k2)) {
+ throw new QueryParseException(
+ String.format(
+ "Cannot use the '%s' argument in conjunction with the '%s' argument", k1, k2));
+ }
}
private static boolean isInt(String s) {
@@ -1089,15 +1120,29 @@
}
private Predicate<ChangeData> ignoredBySelf() throws QueryParseException {
- return new StarPredicate(self(), StarredChangesUtil.IGNORE_LABEL);
+ return ChangePredicates.starBy(
+ args.experimentFeatures.isFeatureEnabled(
+ GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY),
+ args.starredChangesUtil,
+ self(),
+ StarredChangesUtil.IGNORE_LABEL);
}
private Predicate<ChangeData> starredBySelf() throws QueryParseException {
- return new StarPredicate(self(), StarredChangesUtil.DEFAULT_LABEL);
+ return ChangePredicates.starBy(
+ args.experimentFeatures.isFeatureEnabled(
+ GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY),
+ args.starredChangesUtil,
+ self(),
+ StarredChangesUtil.DEFAULT_LABEL);
}
private Predicate<ChangeData> draftBySelf() throws QueryParseException {
- return ChangePredicates.draftBy(self());
+ return ChangePredicates.draftBy(
+ args.experimentFeatures.isFeatureEnabled(
+ GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY),
+ args.commentsUtil,
+ self());
}
@Operator
@@ -1175,9 +1220,7 @@
@Operator
public Predicate<ChangeData> uploader(String who)
throws QueryParseException, IOException, ConfigInvalidException {
- if (!args.getSchema().hasField(ChangeField.UPLOADER)) {
- throw new QueryParseException("'uploader' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.UPLOADER, "uploader");
return uploader(parseAccount(who, (AccountState s) -> true));
}
@@ -1192,10 +1235,7 @@
@Operator
public Predicate<ChangeData> attention(String who)
throws QueryParseException, IOException, ConfigInvalidException {
- if (!args.index.getSchema().hasField(ChangeField.ATTENTION_SET_USERS)) {
- throw new QueryParseException(
- "'attention' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "attention");
return attention(parseAccount(who, (AccountState s) -> true));
}
@@ -1240,9 +1280,7 @@
@Operator
public Predicate<ChangeData> uploaderin(String group) throws QueryParseException, IOException {
- if (!args.getSchema().hasField(ChangeField.UPLOADER)) {
- throw new QueryParseException("'uploader' operator is not supported by change index version");
- }
+ checkFieldAvailable(ChangeField.UPLOADER, "uploaderin");
GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
if (g == null) {
@@ -1287,10 +1325,7 @@
if (Objects.equals(byState, Predicate.<ChangeData>any())) {
return Predicate.any();
}
- if (args.getSchema().hasField(ChangeField.WIP)) {
- return Predicate.and(Predicate.not(new BooleanPredicate(ChangeField.WIP)), byState);
- }
- return byState;
+ return Predicate.and(Predicate.not(new BooleanPredicate(ChangeField.WIP)), byState);
}
@Operator
@@ -1378,7 +1413,7 @@
try (Repository git = args.repoManager.openRepository(args.allUsersName)) {
// [name=]<name>
if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) {
- name = inputArgs.keyValue.get(ARG_ID_NAME);
+ name = inputArgs.keyValue.get(ARG_ID_NAME).value();
} else if (inputArgs.positional.size() == 1) {
name = Iterables.getOnlyElement(inputArgs.positional);
} else if (inputArgs.positional.size() > 1) {
@@ -1387,7 +1422,7 @@
// [,user=<user>]
if (inputArgs.keyValue.containsKey(ARG_ID_USER)) {
- Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER));
+ Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER).value());
if (accounts != null && accounts.size() > 1) {
throw error(
String.format(
@@ -1429,7 +1464,7 @@
try (Repository git = args.repoManager.openRepository(args.allUsersName)) {
// [name=]<name>
if (inputArgs.keyValue.containsKey(ARG_ID_NAME)) {
- name = inputArgs.keyValue.get(ARG_ID_NAME);
+ name = inputArgs.keyValue.get(ARG_ID_NAME).value();
} else if (inputArgs.positional.size() == 1) {
name = Iterables.getOnlyElement(inputArgs.positional);
} else if (inputArgs.positional.size() > 1) {
@@ -1438,7 +1473,7 @@
// [,user=<user>]
if (inputArgs.keyValue.containsKey(ARG_ID_USER)) {
- Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER));
+ Set<Account.Id> accounts = parseAccount(inputArgs.keyValue.get(ARG_ID_USER).value());
if (accounts != null && accounts.size() > 1) {
throw error(
String.format(
@@ -1466,30 +1501,14 @@
@Operator
public Predicate<ChangeData> author(String who) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.EXACT_AUTHOR)) {
- return getAuthorOrCommitterPredicate(
- who.trim(), ChangePredicates::exactAuthor, ChangePredicates::author);
- }
- return getAuthorOrCommitterFullTextPredicate(who.trim(), ChangePredicates::author);
+ return getAuthorOrCommitterPredicate(
+ who.trim(), ChangePredicates::exactAuthor, ChangePredicates::author);
}
@Operator
public Predicate<ChangeData> committer(String who) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.EXACT_COMMITTER)) {
- return getAuthorOrCommitterPredicate(
- who.trim(), ChangePredicates::exactCommitter, ChangePredicates::committer);
- }
- return getAuthorOrCommitterFullTextPredicate(who.trim(), ChangePredicates::committer);
- }
-
- @Operator
- public Predicate<ChangeData> submittable(String str) throws QueryParseException {
- SubmitRecord.Status status =
- Enums.getIfPresent(SubmitRecord.Status.class, str.toUpperCase()).orNull();
- if (status == null) {
- throw error("invalid value for submittable:" + str);
- }
- return new SubmittablePredicate(status);
+ return getAuthorOrCommitterPredicate(
+ who.trim(), ChangePredicates::exactCommitter, ChangePredicates::committer);
}
@Operator
@@ -1502,41 +1521,31 @@
if (value == null || Ints.tryParse(value) == null) {
throw new QueryParseException("'revertof' must be an integer");
}
- if (args.getSchema().hasField(ChangeField.REVERT_OF)) {
- return ChangePredicates.revertOf(Change.id(Ints.tryParse(value)));
- }
- throw new QueryParseException("'revertof' operator is not supported by change index version");
+ return ChangePredicates.revertOf(Change.id(Ints.tryParse(value)));
}
@Operator
- public Predicate<ChangeData> submissionId(String value) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.SUBMISSIONID)) {
- return ChangePredicates.submissionId(value);
- }
- throw new QueryParseException(
- "'submissionid' operator is not supported by change index version");
+ public Predicate<ChangeData> submissionId(String value) {
+ return ChangePredicates.submissionId(value);
}
@Operator
public Predicate<ChangeData> cherryPickOf(String value) throws QueryParseException {
- if (args.getSchema().hasField(ChangeField.CHERRY_PICK_OF_CHANGE)
- && args.getSchema().hasField(ChangeField.CHERRY_PICK_OF_PATCHSET)) {
- if (Ints.tryParse(value) != null) {
- return ChangePredicates.cherryPickOf(Change.id(Ints.tryParse(value)));
- }
- try {
- PatchSet.Id patchSetId = PatchSet.Id.parse(value);
- return ChangePredicates.cherryPickOf(patchSetId);
- } catch (IllegalArgumentException e) {
- throw new QueryParseException(
- "'"
- + value
- + "' is not a valid input. It must be in the 'ChangeNumber[,PatchsetNumber]' format.",
- e);
- }
+ checkFieldAvailable(ChangeField.CHERRY_PICK_OF_CHANGE, "cherryPickOf");
+ checkFieldAvailable(ChangeField.CHERRY_PICK_OF_PATCHSET, "cherryPickOf");
+ if (Ints.tryParse(value) != null) {
+ return ChangePredicates.cherryPickOf(Change.id(Ints.tryParse(value)));
}
- throw new QueryParseException(
- "'cherrypickof' operator is not supported by change index version");
+ try {
+ PatchSet.Id patchSetId = PatchSet.Id.parse(value);
+ return ChangePredicates.cherryPickOf(patchSetId);
+ } catch (IllegalArgumentException e) {
+ throw new QueryParseException(
+ "'"
+ + value
+ + "' is not a valid input. It must be in the 'ChangeNumber[,PatchsetNumber]' format.",
+ e);
+ }
}
@Override
@@ -1595,6 +1604,14 @@
return Predicate.or(predicates);
}
+ protected void checkFieldAvailable(FieldDef<ChangeData, ?> field, String operator)
+ throws QueryParseException {
+ if (!args.index.getSchema().hasField(field)) {
+ throw new QueryParseException(
+ String.format("'%s' operator is not supported by change index version", operator));
+ }
+ }
+
private Predicate<ChangeData> getAuthorOrCommitterPredicate(
String who,
Function<String, Predicate<ChangeData>> exactPredicateFunc,
@@ -1709,11 +1726,9 @@
String who, ReviewerStateInternal state, boolean forDefaultField)
throws QueryParseException, IOException, ConfigInvalidException {
Predicate<ChangeData> reviewerByEmailPredicate = null;
- if (args.index.getSchema().hasField(ChangeField.REVIEWER_BY_EMAIL)) {
- Address address = Address.tryParse(who);
- if (address != null) {
- reviewerByEmailPredicate = ReviewerByEmailPredicate.forState(address, state);
- }
+ Address address = Address.tryParse(who);
+ if (address != null) {
+ reviewerByEmailPredicate = ReviewerByEmailPredicate.forState(address, state);
}
Predicate<ChangeData> reviewerPredicate = null;
diff --git a/java/com/google/gerrit/server/query/change/ConstantPredicate.java b/java/com/google/gerrit/server/query/change/ConstantPredicate.java
new file mode 100644
index 0000000..f0a85fe
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/ConstantPredicate.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.inject.Singleton;
+
+/**
+ * A submit requirement predicate (can only be used in submit requirement expressions) that always
+ * evaluates to {@code true} if the value is equal to "true" or false otherwise.
+ */
+@Singleton
+public class ConstantPredicate extends SubmitRequirementPredicate {
+ public ConstantPredicate(String value) {
+ super("is", value);
+ }
+
+ @Override
+ public boolean match(ChangeData object) {
+ return "true".equals(value);
+ }
+
+ @Override
+ public int getCost() {
+ return 1;
+ }
+}
diff --git a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index 12efecb..a65d0a0 100644
--- a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.change;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Change;
@@ -28,23 +29,41 @@
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.query.change.ChangeData.StorageConstraint;
import java.util.Optional;
public class EqualsLabelPredicate extends ChangeIndexPredicate {
protected final ProjectCache projectCache;
protected final PermissionBackend permissionBackend;
protected final IdentifiedUser.GenericFactory userFactory;
+ /** label name to be matched. */
protected final String label;
+
+ /** Expected vote value for the label. */
protected final int expVal;
+
+ /**
+ * Number of times the value {@link #expVal} for label {@link #label} should occur. If null, match
+ * with any count greater or equal to 1.
+ */
+ @Nullable protected final Integer count;
+
+ /** Account ID that has voted on the label. */
protected final Account.Id account;
+
protected final AccountGroup.UUID group;
public EqualsLabelPredicate(
- LabelPredicate.Args args, String label, int expVal, Account.Id account) {
- super(ChangeField.LABEL, ChangeField.formatLabel(label, expVal, account));
+ LabelPredicate.Args args,
+ String label,
+ int expVal,
+ Account.Id account,
+ @Nullable Integer count) {
+ super(ChangeField.LABEL, ChangeField.formatLabel(label, expVal, account, count));
this.permissionBackend = args.permissionBackend;
this.projectCache = args.projectCache;
this.userFactory = args.userFactory;
+ this.count = count;
this.group = args.group;
this.label = label;
this.expVal = expVal;
@@ -60,6 +79,14 @@
return false;
}
+ if (Integer.valueOf(0).equals(count)) {
+ // We don't match against count=0 so that the computation is identical to the stored values
+ // in the index. We do that since computing count=0 requires looping on all {label_type,
+ // vote_value} for the change and storing a {count=0} format for it in the change index which
+ // is computationally expensive.
+ return false;
+ }
+
Optional<ProjectState> project = projectCache.get(c.getDest().project());
if (!project.isPresent()) {
// The project has disappeared.
@@ -73,21 +100,23 @@
}
boolean hasVote = false;
+ int matchingVotes = 0;
+ StorageConstraint currentStorageConstraint = object.getStorageConstraint();
object.setStorageConstraint(ChangeData.StorageConstraint.INDEX_PRIMARY_NOTEDB_SECONDARY);
for (PatchSetApproval p : object.currentApprovals()) {
if (labelType.matches(p)) {
hasVote = true;
if (match(object, p.value(), p.accountId())) {
- return true;
+ matchingVotes += 1;
}
}
}
-
+ object.setStorageConstraint(currentStorageConstraint);
if (!hasVote && expVal == 0) {
return true;
}
- return false;
+ return count == null ? matchingVotes >= 1 : matchingVotes == count;
}
protected static LabelType type(LabelTypes types, String toFind) {
diff --git a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index c8e9f66..3f8bfda 100644
--- a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -22,6 +22,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
@@ -294,7 +295,7 @@
return and(project(project), or(groupPredicates));
}
- public static List<ChangeData> byProjectGroups(
+ public static ImmutableList<ChangeData> byProjectGroups(
Provider<InternalChangeQuery> queryProvider,
IndexConfig indexConfig,
Project.NameKey project,
@@ -323,6 +324,6 @@
}
}
}
- return result;
+ return ImmutableList.copyOf(result);
}
}
diff --git a/java/com/google/gerrit/server/query/change/IsSubmittablePredicate.java b/java/com/google/gerrit/server/query/change/IsSubmittablePredicate.java
new file mode 100644
index 0000000..17de132
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/IsSubmittablePredicate.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.index.query.NotPredicate;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.server.index.change.ChangeField;
+
+public class IsSubmittablePredicate extends BooleanPredicate {
+ public IsSubmittablePredicate() {
+ super(ChangeField.IS_SUBMITTABLE);
+ }
+
+ /**
+ * Rewrite the is:submittable predicate.
+ *
+ * <p>If we run a query with "is:submittable OR -is:submittable" the result should match all
+ * changes. In Lucene, we keep separate sub-indexes for open and closed changes. The Lucene
+ * backend inspects the input predicate and depending on all its child predicates decides if the
+ * query should run against the open sub-index, closed sub-index or both.
+ *
+ * <p>The "is:submittable" operator is implemented as:
+ *
+ * <p>issubmittable:1
+ *
+ * <p>But we want to exclude closed changes from being matched by this query. For the normal case,
+ * we rewrite the query as:
+ *
+ * <p>issubmittable:1 AND status:new
+ *
+ * <p>Hence Lucene will match the query against the open sub-index. For the negated case (i.e.
+ * "-is:submittable"), we cannot just negate the previous query because it would result in:
+ *
+ * <p>-(issubmittable:1 AND status:new)
+ *
+ * <p>Lucene will conclude that it should look for changes that are <b>not</b> new and hence will
+ * run the query against the closed sub-index, not matching with changes that are open but not
+ * submittable. For this case, we need to rewrite the query to match with closed changes <b>or</b>
+ * changes that are not submittable.
+ */
+ public static Predicate<ChangeData> rewrite(Predicate<ChangeData> in) {
+ if (in instanceof IsSubmittablePredicate) {
+ return Predicate.and(
+ new BooleanPredicate(ChangeField.IS_SUBMITTABLE), ChangeStatusPredicate.open());
+ }
+ if (in instanceof NotPredicate && in.getChild(0) instanceof IsSubmittablePredicate) {
+ return Predicate.or(
+ Predicate.not(new BooleanPredicate(ChangeField.IS_SUBMITTABLE)),
+ ChangeStatusPredicate.closed());
+ }
+ return in;
+ }
+}
diff --git a/java/com/google/gerrit/server/query/change/LabelPredicate.java b/java/com/google/gerrit/server/query/change/LabelPredicate.java
index 5f017fb..2a5a47d 100644
--- a/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -14,8 +14,8 @@
package com.google.gerrit.server.query.change;
-import com.google.common.collect.ImmutableList;
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.index.query.OrPredicate;
@@ -29,9 +29,11 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
+import java.util.stream.IntStream;
public class LabelPredicate extends OrPredicate<ChangeData> {
protected static final int MAX_LABEL_VALUE = 4;
+ protected static final int MAX_COUNT = 5; // inclusive
protected static class Args {
protected final ProjectCache projectCache;
@@ -40,6 +42,8 @@
protected final String value;
protected final Set<Account.Id> accounts;
protected final AccountGroup.UUID group;
+ protected final Integer count;
+ protected final PredicateArgs.Operator countOp;
protected Args(
ProjectCache projectCache,
@@ -47,13 +51,17 @@
IdentifiedUser.GenericFactory userFactory,
String value,
Set<Account.Id> accounts,
- AccountGroup.UUID group) {
+ AccountGroup.UUID group,
+ @Nullable Integer count,
+ @Nullable PredicateArgs.Operator countOp) {
this.projectCache = projectCache;
this.permissionBackend = permissionBackend;
this.userFactory = userFactory;
this.value = value;
this.accounts = accounts;
this.group = group;
+ this.count = count;
+ this.countOp = countOp;
}
}
@@ -75,19 +83,35 @@
ChangeQueryBuilder.Arguments a,
String value,
Set<Account.Id> accounts,
- AccountGroup.UUID group) {
+ AccountGroup.UUID group,
+ @Nullable Integer count,
+ @Nullable PredicateArgs.Operator countOp) {
super(
predicates(
- new Args(a.projectCache, a.permissionBackend, a.userFactory, value, accounts, group)));
+ new Args(
+ a.projectCache,
+ a.permissionBackend,
+ a.userFactory,
+ value,
+ accounts,
+ group,
+ count,
+ countOp)));
this.value = value;
}
protected static List<Predicate<ChangeData>> predicates(Args args) {
String v = args.value;
-
+ List<Integer> counts = getCounts(args.count, args.countOp);
try {
MagicLabelVote mlv = MagicLabelVote.parseWithEquals(v);
- return ImmutableList.of(magicLabelPredicate(args, mlv));
+ List<Predicate<ChangeData>> result = Lists.newArrayListWithCapacity(counts.size());
+ if (counts.isEmpty()) {
+ result.add(magicLabelPredicate(args, mlv, /* count= */ null));
+ } else {
+ counts.forEach(count -> result.add(magicLabelPredicate(args, mlv, count)));
+ }
+ return result;
} catch (IllegalArgumentException e) {
// Try next format.
}
@@ -123,16 +147,24 @@
int min = range.min;
int max = range.max;
- List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(max - min + 1);
+ List<Predicate<ChangeData>> r =
+ Lists.newArrayListWithCapacity((counts.isEmpty() ? 1 : counts.size()) * (max - min + 1));
for (int i = min; i <= max; i++) {
- r.add(onePredicate(args, prefix, i));
+ if (counts.isEmpty()) {
+ r.add(onePredicate(args, prefix, i, /* count= */ null));
+ } else {
+ for (int count : counts) {
+ r.add(onePredicate(args, prefix, i, count));
+ }
+ }
}
return r;
}
- protected static Predicate<ChangeData> onePredicate(Args args, String label, int expVal) {
+ protected static Predicate<ChangeData> onePredicate(
+ Args args, String label, int expVal, @Nullable Integer count) {
if (expVal != 0) {
- return equalsLabelPredicate(args, label, expVal);
+ return equalsLabelPredicate(args, label, expVal, count);
}
return noLabelQuery(args, label);
}
@@ -140,34 +172,69 @@
protected static Predicate<ChangeData> noLabelQuery(Args args, String label) {
List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(2 * MAX_LABEL_VALUE);
for (int i = 1; i <= MAX_LABEL_VALUE; i++) {
- r.add(equalsLabelPredicate(args, label, i));
- r.add(equalsLabelPredicate(args, label, -i));
+ r.add(equalsLabelPredicate(args, label, i, /* count= */ null));
+ r.add(equalsLabelPredicate(args, label, -i, /* count= */ null));
}
return not(or(r));
}
- protected static Predicate<ChangeData> equalsLabelPredicate(Args args, String label, int expVal) {
+ protected static Predicate<ChangeData> equalsLabelPredicate(
+ Args args, String label, int expVal, @Nullable Integer count) {
if (args.accounts == null || args.accounts.isEmpty()) {
- return new EqualsLabelPredicate(args, label, expVal, null);
+ return new EqualsLabelPredicate(args, label, expVal, null, count);
}
List<Predicate<ChangeData>> r = new ArrayList<>();
for (Account.Id a : args.accounts) {
- r.add(new EqualsLabelPredicate(args, label, expVal, a));
+ r.add(new EqualsLabelPredicate(args, label, expVal, a, count));
}
return or(r);
}
- protected static Predicate<ChangeData> magicLabelPredicate(Args args, MagicLabelVote mlv) {
+ protected static Predicate<ChangeData> magicLabelPredicate(
+ Args args, MagicLabelVote mlv, @Nullable Integer count) {
if (args.accounts == null || args.accounts.isEmpty()) {
- return new MagicLabelPredicate(args, mlv, /* account= */ null);
+ return new MagicLabelPredicate(args, mlv, /* account= */ null, count);
}
List<Predicate<ChangeData>> r = new ArrayList<>();
for (Account.Id a : args.accounts) {
- r.add(new MagicLabelPredicate(args, mlv, a));
+ r.add(new MagicLabelPredicate(args, mlv, a, count));
}
return or(r);
}
+ private static List<Integer> getCounts(
+ @Nullable Integer count, @Nullable PredicateArgs.Operator countOp) {
+ List<Integer> result = new ArrayList<>();
+ if (count == null) {
+ return result;
+ }
+ switch (countOp) {
+ case EQUAL:
+ case GREATER_EQUAL:
+ case LESS_EQUAL:
+ result.add(count);
+ break;
+ case GREATER:
+ case LESS:
+ default:
+ break;
+ }
+ switch (countOp) {
+ case GREATER:
+ case GREATER_EQUAL:
+ IntStream.range(count + 1, MAX_COUNT + 1).forEach(result::add);
+ break;
+ case LESS:
+ case LESS_EQUAL:
+ IntStream.range(0, count).forEach(result::add);
+ break;
+ case EQUAL:
+ default:
+ break;
+ }
+ return result;
+ }
+
@Override
public String toString() {
return ChangeQueryBuilder.FIELD_LABEL + ":" + value;
diff --git a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
index 3917c79..5a81ca1 100644
--- a/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/MagicLabelPredicate.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.change;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelType;
@@ -30,13 +31,21 @@
protected final LabelPredicate.Args args;
private final MagicLabelVote magicLabelVote;
private final Account.Id account;
+ @Nullable private final Integer count;
public MagicLabelPredicate(
- LabelPredicate.Args args, MagicLabelVote magicLabelVote, Account.Id account) {
- super(ChangeField.LABEL, magicLabelVote.formatLabel());
+ LabelPredicate.Args args,
+ MagicLabelVote magicLabelVote,
+ Account.Id account,
+ @Nullable Integer count) {
+ super(
+ ChangeField.LABEL,
+ ChangeField.formatLabel(
+ magicLabelVote.label(), magicLabelVote.value().name(), account, count));
this.account = account;
this.args = args;
this.magicLabelVote = magicLabelVote;
+ this.count = count;
}
@Override
@@ -87,7 +96,7 @@
}
private EqualsLabelPredicate numericPredicate(String label, short value) {
- return new EqualsLabelPredicate(args, label, value, account);
+ return new EqualsLabelPredicate(args, label, value, account, count);
}
protected static LabelType type(LabelTypes types, String toFind) {
diff --git a/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java b/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
index 4a54c03..4f181a4 100644
--- a/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.change;
+import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.common.ProjectInfo;
@@ -24,7 +25,6 @@
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -39,11 +39,11 @@
this.value = value;
}
- protected static List<Predicate<ChangeData>> predicates(
+ protected static ImmutableList<Predicate<ChangeData>> predicates(
ProjectCache projectCache, ChildProjects childProjects, String value) {
Optional<ProjectState> projectState = projectCache.get(Project.nameKey(value));
if (!projectState.isPresent()) {
- return Collections.emptyList();
+ return ImmutableList.of();
}
List<Predicate<ChangeData>> r = new ArrayList<>();
@@ -55,7 +55,7 @@
} catch (PermissionBackendException e) {
logger.atWarning().withCause(e).log("cannot check permissions to expand child projects");
}
- return r;
+ return ImmutableList.copyOf(r);
}
@Override
diff --git a/java/com/google/gerrit/server/query/change/PredicateArgs.java b/java/com/google/gerrit/server/query/change/PredicateArgs.java
index d82b9bc..ebe4390 100644
--- a/java/com/google/gerrit/server/query/change/PredicateArgs.java
+++ b/java/com/google/gerrit/server/query/change/PredicateArgs.java
@@ -14,12 +14,15 @@
package com.google.gerrit.server.query.change;
+import com.google.auto.value.AutoValue;
import com.google.common.base.Splitter;
import com.google.gerrit.index.query.QueryParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* This class is used to extract comma separated values in a predicate.
@@ -30,8 +33,35 @@
* appear in the map and others in the positional list (e.g. "vote=approved,jb_2.3).
*/
public class PredicateArgs {
+ private static final Pattern SPLIT_PATTERN = Pattern.compile("(>|>=|=|<|<=)([^=].*)$");
+
public List<String> positional;
- public Map<String, String> keyValue;
+ public Map<String, ValOp> keyValue;
+
+ enum Operator {
+ EQUAL("="),
+ GREATER_EQUAL(">="),
+ GREATER(">"),
+ LESS_EQUAL("<="),
+ LESS("<");
+
+ final String op;
+
+ Operator(String op) {
+ this.op = op;
+ }
+ }
+
+ @AutoValue
+ public abstract static class ValOp {
+ abstract String value();
+
+ abstract Operator operator();
+
+ static ValOp create(String value, Operator operator) {
+ return new AutoValue_PredicateArgs_ValOp(value, operator);
+ }
+ }
/**
* Parses query arguments into {@link #keyValue} and/or {@link #positional}..
@@ -46,19 +76,39 @@
keyValue = new HashMap<>();
for (String arg : Splitter.on(',').split(args)) {
- List<String> splitKeyValue = Splitter.on('=').splitToList(arg);
+ Matcher m = SPLIT_PATTERN.matcher(arg);
- if (splitKeyValue.size() == 1) {
- positional.add(splitKeyValue.get(0));
- } else if (splitKeyValue.size() == 2) {
- if (!keyValue.containsKey(splitKeyValue.get(0))) {
- keyValue.put(splitKeyValue.get(0), splitKeyValue.get(1));
+ if (!m.find()) {
+ positional.add(arg);
+ } else if (m.groupCount() == 2) {
+ String key = arg.substring(0, m.start());
+ String op = m.group(1);
+ String val = m.group(2);
+ if (!keyValue.containsKey(key)) {
+ keyValue.put(key, ValOp.create(val, getOperator(op)));
} else {
- throw new QueryParseException("Duplicate key " + splitKeyValue.get(0));
+ throw new QueryParseException("Duplicate key " + key);
}
} else {
- throw new QueryParseException("invalid arg " + arg);
+ throw new QueryParseException("Invalid arg " + arg);
}
}
}
+
+ private Operator getOperator(String operator) {
+ switch (operator) {
+ case "<":
+ return Operator.LESS;
+ case "<=":
+ return Operator.LESS_EQUAL;
+ case "=":
+ return Operator.EQUAL;
+ case ">=":
+ return Operator.GREATER_EQUAL;
+ case ">":
+ return Operator.GREATER;
+ default:
+ throw new IllegalArgumentException("Invalid Operator " + operator);
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
index e63714f..1750be0 100644
--- a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
@@ -14,7 +14,10 @@
package com.google.gerrit.server.query.change;
+import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryBuilder;
+import com.google.gerrit.index.query.QueryParseException;
import com.google.inject.Inject;
/**
@@ -32,4 +35,23 @@
SubmitRequirementChangeQueryBuilder(Arguments args) {
super(def, args);
}
+
+ @Override
+ protected void checkFieldAvailable(FieldDef<ChangeData, ?> field, String operator) {
+ // Submit requirements don't rely on the index, so they can be used regardless of index schema
+ // version.
+ }
+
+ @Override
+ public Predicate<ChangeData> is(String value) throws QueryParseException {
+ if ("submittable".equalsIgnoreCase(value)) {
+ throw new QueryParseException(
+ String.format(
+ "Operator 'is:submittable' cannot be used in submit requirement expressions."));
+ }
+ if ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value)) {
+ return new ConstantPredicate(value);
+ }
+ return super.is(value);
+ }
}
diff --git a/java/com/google/gerrit/server/restapi/access/AccessResource.java b/java/com/google/gerrit/server/restapi/access/AccessResource.java
index 4847da4..36ffdcf 100644
--- a/java/com/google/gerrit/server/restapi/access/AccessResource.java
+++ b/java/com/google/gerrit/server/restapi/access/AccessResource.java
@@ -26,6 +26,5 @@
* collection.
*/
public class AccessResource implements RestResource {
- public static final TypeLiteral<RestView<AccessResource>> ACCESS_KIND =
- new TypeLiteral<RestView<AccessResource>>() {};
+ public static final TypeLiteral<RestView<AccessResource>> ACCESS_KIND = new TypeLiteral<>() {};
}
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
index 1485a6e56..fbd99eb 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.restapi.account;
import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.gerrit.server.experiments.ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY;
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
@@ -39,6 +40,7 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.change.ChangeJson;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangePredicates;
@@ -54,7 +56,7 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
@@ -75,6 +77,7 @@
private final Provider<CommentJson> commentJsonProvider;
private final CommentsUtil commentsUtil;
private final PatchSetUtil psUtil;
+ private final ExperimentFeatures experimentFeatures;
@Inject
DeleteDraftComments(
@@ -86,7 +89,8 @@
ChangeJson.Factory changeJsonFactory,
Provider<CommentJson> commentJsonProvider,
CommentsUtil commentsUtil,
- PatchSetUtil psUtil) {
+ PatchSetUtil psUtil,
+ ExperimentFeatures experimentFeatures) {
this.userProvider = userProvider;
this.batchUpdateFactory = batchUpdateFactory;
this.queryBuilderProvider = queryBuilderProvider;
@@ -96,6 +100,7 @@
this.commentJsonProvider = commentJsonProvider;
this.commentsUtil = commentsUtil;
this.psUtil = psUtil;
+ this.experimentFeatures = experimentFeatures;
}
@Override
@@ -118,7 +123,7 @@
HumanCommentFormatter humanCommentFormatter =
commentJsonProvider.get().newHumanCommentFormatter();
Account.Id accountId = rsrc.getUser().getAccountId();
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
Map<Project.NameKey, BatchUpdate> updates = new LinkedHashMap<>();
List<Op> ops = new ArrayList<>();
for (ChangeData cd :
@@ -146,7 +151,12 @@
private Predicate<ChangeData> predicate(Account.Id accountId, DeleteDraftCommentsInput input)
throws BadRequestException {
- Predicate<ChangeData> hasDraft = ChangePredicates.draftBy(accountId);
+ Predicate<ChangeData> hasDraft =
+ ChangePredicates.draftBy(
+ experimentFeatures.isFeatureEnabled(
+ GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY),
+ commentsUtil,
+ accountId);
if (CharMatcher.whitespace().trimFrom(Strings.nullToEmpty(input.query)).isEmpty()) {
return hasDraft;
}
diff --git a/java/com/google/gerrit/server/restapi/account/GetAvatar.java b/java/com/google/gerrit/server/restapi/account/GetAvatar.java
index 5256d68..14fee12 100644
--- a/java/com/google/gerrit/server/restapi/account/GetAvatar.java
+++ b/java/com/google/gerrit/server/restapi/account/GetAvatar.java
@@ -55,12 +55,12 @@
public Response.Redirect apply(AccountResource rsrc) throws ResourceNotFoundException {
AvatarProvider impl = avatarProvider.get();
if (impl == null) {
- throw (new ResourceNotFoundException()).caching(CacheControl.PUBLIC(1, TimeUnit.DAYS));
+ throw new ResourceNotFoundException().caching(CacheControl.PUBLIC(1, TimeUnit.DAYS));
}
String url = impl.getUrl(rsrc.getUser(), size);
if (Strings.isNullOrEmpty(url)) {
- throw (new ResourceNotFoundException()).caching(CacheControl.PUBLIC(1, TimeUnit.HOURS));
+ throw new ResourceNotFoundException().caching(CacheControl.PUBLIC(1, TimeUnit.HOURS));
}
return Response.redirect(url);
}
diff --git a/java/com/google/gerrit/server/restapi/account/GetDetail.java b/java/com/google/gerrit/server/restapi/account/GetDetail.java
index 1091599..ba7a37f 100644
--- a/java/com/google/gerrit/server/restapi/account/GetDetail.java
+++ b/java/com/google/gerrit/server/restapi/account/GetDetail.java
@@ -48,7 +48,7 @@
public Response<AccountDetailInfo> apply(AccountResource rsrc) throws PermissionBackendException {
Account a = rsrc.getUser().getAccount();
AccountDetailInfo info = new AccountDetailInfo(a.id().get());
- info.registeredOn = a.registeredOn();
+ info.setRegisteredOn(a.registeredOn());
info.inactive = !a.isActive() ? true : null;
directory.fillAccountInfo(Collections.singleton(info), EnumSet.allOf(FillOptions.class));
return Response.ok(info);
diff --git a/java/com/google/gerrit/server/restapi/change/Abandon.java b/java/com/google/gerrit/server/restapi/change/Abandon.java
index 2cfc3f5..98514b2 100644
--- a/java/com/google/gerrit/server/restapi/change/Abandon.java
+++ b/java/com/google/gerrit/server/restapi/change/Abandon.java
@@ -30,8 +30,10 @@
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.StoreSubmitRequirementsOp;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.UpdateException;
import com.google.gerrit.server.util.time.TimeUtil;
@@ -43,24 +45,30 @@
@Singleton
public class Abandon
implements RestModifyView<ChangeResource, AbandonInput>, UiAction<ChangeResource> {
+ private final ChangeData.Factory changeDataFactory;
private final BatchUpdate.Factory updateFactory;
private final ChangeJson.Factory json;
private final AbandonOp.Factory abandonOpFactory;
private final NotifyResolver notifyResolver;
private final PatchSetUtil patchSetUtil;
+ private final StoreSubmitRequirementsOp.Factory storeSubmitRequirementsOpFactory;
@Inject
Abandon(
+ ChangeData.Factory changeDataFactory,
BatchUpdate.Factory updateFactory,
ChangeJson.Factory json,
AbandonOp.Factory abandonOpFactory,
NotifyResolver notifyResolver,
- PatchSetUtil patchSetUtil) {
+ PatchSetUtil patchSetUtil,
+ StoreSubmitRequirementsOp.Factory storeSubmitRequirementsOpFactory) {
+ this.changeDataFactory = changeDataFactory;
this.updateFactory = updateFactory;
this.json = json;
this.abandonOpFactory = abandonOpFactory;
this.notifyResolver = notifyResolver;
this.patchSetUtil = patchSetUtil;
+ this.storeSubmitRequirementsOpFactory = storeSubmitRequirementsOpFactory;
}
@Override
@@ -117,9 +125,14 @@
throws RestApiException, UpdateException {
AccountState accountState = user.isIdentifiedUser() ? user.asIdentifiedUser().state() : null;
AbandonOp op = abandonOpFactory.create(accountState, msgTxt);
- try (BatchUpdate u = updateFactory.create(notes.getProjectName(), user, TimeUtil.nowTs())) {
+ ChangeData changeData = changeDataFactory.create(notes.getProjectName(), notes.getChangeId());
+ try (BatchUpdate u = updateFactory.create(notes.getProjectName(), user, TimeUtil.now())) {
u.setNotify(notify);
- u.addOp(notes.getChangeId(), op).execute();
+ u.addOp(notes.getChangeId(), op);
+ u.addOp(
+ notes.getChangeId(),
+ storeSubmitRequirementsOpFactory.create(changeData.submitRequirements().values()));
+ u.execute();
}
return op.getChange();
}
diff --git a/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java b/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java
index cb1256c..a21431e 100644
--- a/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java
+++ b/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java
@@ -92,7 +92,7 @@
try (BatchUpdate bu =
updateFactory.create(
- changeResource.getChange().getProject(), changeResource.getUser(), TimeUtil.nowTs())) {
+ changeResource.getChange().getProject(), changeResource.getUser(), TimeUtil.now())) {
AddToAttentionSetOp op = opFactory.create(attentionUserId, input.reason, true);
bu.addOp(changeResource.getId(), op);
NotifyHandling notify = input.notify == null ? NotifyHandling.OWNER : input.notify;
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
index 318b0fa..19cfd6a 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
@@ -127,10 +127,11 @@
}
@Override
- public Response<Object> apply(ChangeResource resource, IdString id, FileContentInput input)
+ public Response<Object> apply(
+ ChangeResource resource, IdString id, FileContentInput fileContentInput)
throws AuthException, BadRequestException, ResourceConflictException, IOException,
PermissionBackendException {
- putEdit.apply(resource, id.get(), input);
+ putEdit.apply(resource, id.get(), fileContentInput);
return Response.none();
}
}
@@ -146,7 +147,7 @@
}
@Override
- public Response<Object> apply(ChangeResource rsrc, IdString id, Input in)
+ public Response<Object> apply(ChangeResource rsrc, IdString id, Input input)
throws IOException, AuthException, BadRequestException, ResourceConflictException,
PermissionBackendException {
return deleteContent.apply(rsrc, id.get());
@@ -249,15 +250,16 @@
}
@Override
- public Response<Object> apply(ChangeResource resource, Post.Input input)
+ public Response<Object> apply(ChangeResource resource, Post.Input postInput)
throws AuthException, BadRequestException, IOException, ResourceConflictException,
PermissionBackendException {
Project.NameKey project = resource.getProject();
try (Repository repository = repositoryManager.openRepository(project)) {
- if (isRestoreFile(input)) {
- editModifier.restoreFile(repository, resource.getNotes(), input.restorePath);
- } else if (isRenameFile(input)) {
- editModifier.renameFile(repository, resource.getNotes(), input.oldPath, input.newPath);
+ if (isRestoreFile(postInput)) {
+ editModifier.restoreFile(repository, resource.getNotes(), postInput.restorePath);
+ } else if (isRenameFile(postInput)) {
+ editModifier.renameFile(
+ repository, resource.getNotes(), postInput.oldPath, postInput.newPath);
} else {
editModifier.createEdit(repository, resource.getNotes());
}
@@ -267,14 +269,14 @@
return Response.none();
}
- private static boolean isRestoreFile(Input input) {
- return input != null && !Strings.isNullOrEmpty(input.restorePath);
+ private static boolean isRestoreFile(Post.Input postInput) {
+ return postInput != null && !Strings.isNullOrEmpty(postInput.restorePath);
}
- private static boolean isRenameFile(Input input) {
- return input != null
- && !Strings.isNullOrEmpty(input.oldPath)
- && !Strings.isNullOrEmpty(input.newPath);
+ private static boolean isRenameFile(Post.Input postInput) {
+ return postInput != null
+ && !Strings.isNullOrEmpty(postInput.oldPath)
+ && !Strings.isNullOrEmpty(postInput.newPath);
}
}
@@ -300,37 +302,38 @@
}
@Override
- public Response<Object> apply(ChangeEditResource rsrc, FileContentInput input)
+ public Response<Object> apply(ChangeEditResource rsrc, FileContentInput fileContentInput)
throws AuthException, BadRequestException, ResourceConflictException, IOException,
PermissionBackendException {
- return apply(rsrc.getChangeResource(), rsrc.getPath(), input);
+ return apply(rsrc.getChangeResource(), rsrc.getPath(), fileContentInput);
}
- public Response<Object> apply(ChangeResource rsrc, String path, FileContentInput input)
+ public Response<Object> apply(
+ ChangeResource rsrc, String path, FileContentInput fileContentInput)
throws AuthException, BadRequestException, ResourceConflictException, IOException,
PermissionBackendException {
- if (input.content == null && input.binary_content == null) {
+ if (fileContentInput.content == null && fileContentInput.binary_content == null) {
throw new BadRequestException("either content or binary_content is required");
}
RawInput newContent;
- if (input.binary_content != null) {
- Matcher m = BINARY_DATA_PATTERN.matcher(input.binary_content);
+ if (fileContentInput.binary_content != null) {
+ Matcher m = BINARY_DATA_PATTERN.matcher(fileContentInput.binary_content);
if (m.matches() && BASE64.equals(m.group(2))) {
newContent = RawInputUtil.create(Base64.decode(m.group(3)));
} else {
throw new BadRequestException("binary_content must be encoded as base64 data uri");
}
} else {
- newContent = input.content;
+ newContent = fileContentInput.content;
}
- if (Patch.COMMIT_MSG.equals(path) && input.binary_content == null) {
- EditMessage.Input editCommitMessageInput = new EditMessage.Input();
- editCommitMessageInput.message =
+ if (Patch.COMMIT_MSG.equals(path) && fileContentInput.binary_content == null) {
+ EditMessage.Input editMessageInput = new EditMessage.Input();
+ editMessageInput.message =
new String(ByteStreams.toByteArray(newContent.getInputStream()), UTF_8);
- return editMessage.apply(rsrc, editCommitMessageInput);
+ return editMessage.apply(rsrc, editMessageInput);
}
if (Strings.isNullOrEmpty(path) || path.charAt(0) == '/') {
@@ -471,16 +474,16 @@
}
@Override
- public Response<Object> apply(ChangeResource rsrc, Input input)
+ public Response<Object> apply(ChangeResource rsrc, EditMessage.Input editMessageInput)
throws AuthException, IOException, BadRequestException, ResourceConflictException,
PermissionBackendException {
- if (input == null || Strings.isNullOrEmpty(input.message)) {
+ if (editMessageInput == null || Strings.isNullOrEmpty(editMessageInput.message)) {
throw new BadRequestException("commit message must be provided");
}
Project.NameKey project = rsrc.getProject();
try (Repository repository = repositoryManager.openRepository(project)) {
- editModifier.modifyMessage(repository, rsrc.getNotes(), input.message);
+ editModifier.modifyMessage(repository, rsrc.getNotes(), editMessageInput.message);
} 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 02b4c13..e09f2f4 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
@@ -143,7 +143,6 @@
get(REVISION_KIND, "related").to(GetRelated.class);
get(REVISION_KIND, "review").to(GetReview.class);
post(REVISION_KIND, "review").to(PostReview.class);
- get(REVISION_KIND, "preview_submit").to(PreviewSubmit.class);
post(REVISION_KIND, "submit").to(Submit.class);
post(REVISION_KIND, "rebase").to(Rebase.class);
put(REVISION_KIND, "description").to(PutDescription.class);
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index 5375936..0fc5716 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -67,7 +67,7 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -170,7 +170,7 @@
patch.commitId(),
input,
dest,
- TimeUtil.nowTs(),
+ TimeUtil.now(),
null,
null,
null,
@@ -205,7 +205,7 @@
throws IOException, InvalidChangeOperationException, UpdateException, RestApiException,
ConfigInvalidException, NoSuchProjectException {
return cherryPick(
- sourceChange, project, sourceCommit, input, dest, TimeUtil.nowTs(), null, null, null, null);
+ sourceChange, project, sourceCommit, input, dest, TimeUtil.now(), null, null, null, null);
}
/**
@@ -243,7 +243,7 @@
ObjectId sourceCommit,
CherryPickInput input,
BranchNameKey dest,
- Timestamp timestamp,
+ Instant timestamp,
@Nullable Change.Id revertedChange,
@Nullable ObjectId changeIdForNewChange,
@Nullable Change.Id idForNewChange,
diff --git a/java/com/google/gerrit/server/restapi/change/CommentPorter.java b/java/com/google/gerrit/server/restapi/change/CommentPorter.java
index 1a4eb18..24a9c83 100644
--- a/java/com/google/gerrit/server/restapi/change/CommentPorter.java
+++ b/java/com/google/gerrit/server/restapi/change/CommentPorter.java
@@ -40,6 +40,7 @@
import com.google.gerrit.server.patch.DiffMappings;
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.GitPositionTransformer;
import com.google.gerrit.server.patch.GitPositionTransformer.BestPositionOnConflict;
import com.google.gerrit.server.patch.GitPositionTransformer.FileMapping;
@@ -193,10 +194,9 @@
patchsetComments));
} else {
logger.atWarning().log(
- String.format(
- "Some comments which should be ported refer to the non-existent patchset %s of"
- + " change %d. Omitting %d affected comments.",
- originalPatchsetId, notes.getChangeId().get(), patchsetComments.size()));
+ "Some comments which should be ported refer to the non-existent patchset %s of"
+ + " change %d. Omitting %d affected comments.",
+ originalPatchsetId, notes.getChangeId().get(), patchsetComments.size());
}
}
return portedComments.build();
@@ -254,8 +254,8 @@
mappings = loadMappings(project, change, originalPatchset, targetPatchset, side);
} catch (Exception e) {
logger.atWarning().withCause(e).log(
- "Could not determine some necessary diff mappings for porting comments on change %s from"
- + " patchset %s to patchset %s. Mapping %d affected comments to the fallback"
+ "Could not determine some necessary diff mappings for porting comments on change %s"
+ + " from patchset %s to patchset %s. Mapping %d affected comments to the fallback"
+ " destination.",
change.getChangeId(),
originalPatchset.id().getId(),
@@ -315,7 +315,11 @@
TraceContext.newTimer(
"Computing diffs", Metadata.builder().commit(originalCommit.name()).build())) {
Map<String, FileDiffOutput> modifiedFiles =
- diffOperations.listModifiedFiles(project, originalCommit, targetCommit);
+ diffOperations.listModifiedFiles(
+ project,
+ originalCommit,
+ targetCommit,
+ DiffOptions.builder().skipFilesWithAllEditsDueToRebase(false).build());
return modifiedFiles.values().stream()
.map(CommentPorter::getFileEdits)
.map(DiffMappings::toMapping)
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index fa47bef..6a637b3 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -84,8 +84,9 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Collections;
+import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.TimeZone;
@@ -319,6 +320,9 @@
}
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
private ChangeInfo createNewChange(
ChangeInput input,
IdentifiedUser me,
@@ -353,13 +357,14 @@
RevCommit mergeTip = parentCommit == null ? null : rw.parseCommit(parentCommit);
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
PersonIdent committer = me.newCommitterIdent(now, serverTimeZone);
PersonIdent author =
input.author == null
? committer
- : new PersonIdent(input.author.name, input.author.email, now, serverTimeZone);
+ : new PersonIdent(
+ input.author.name, input.author.email, Date.from(now), serverTimeZone);
String commitMessage = getCommitMessage(input.subject, me);
diff --git a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
index 8476767..9e9cf6a 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
@@ -82,8 +82,7 @@
String.format("Invalid inReplyTo, comment %s not found", in.inReplyTo));
}
- try (BatchUpdate bu =
- updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
Op op = new Op(rsrc.getPatchSet().id(), in);
bu.addOp(rsrc.getChange().getId(), op);
bu.execute();
diff --git a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
index e943e47..651bf7b 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
@@ -67,7 +67,8 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import org.eclipse.jgit.lib.ObjectId;
@@ -122,6 +123,9 @@
this.permissionBackend = permissionBackend;
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Override
public Response<ChangeInfo> apply(ChangeResource rsrc, MergePatchSetInput in)
throws IOException, RestApiException, UpdateException, PermissionBackendException {
@@ -176,12 +180,12 @@
currentPsCommit = rw.parseCommit(ps.commitId());
}
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
IdentifiedUser me = user.get().asIdentifiedUser();
PersonIdent author =
in.author == null
? me.newCommitterIdent(now, serverTimeZone)
- : new PersonIdent(in.author.name, in.author.email, now, serverTimeZone);
+ : new PersonIdent(in.author.name, in.author.email, Date.from(now), serverTimeZone);
CodeReviewCommit newCommit =
createMergeCommit(
in,
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
index d867e00..d818210 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
@@ -67,8 +67,7 @@
throws RestApiException, UpdateException, PermissionBackendException {
rsrc.permissions().check(ChangePermission.EDIT_ASSIGNEE);
- try (BatchUpdate bu =
- updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
Op op = new Op();
bu.addOp(rsrc.getChange().getId(), op);
bu.execute();
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChange.java b/java/com/google/gerrit/server/restapi/change/DeleteChange.java
index 3ca5463..8298abb 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChange.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChange.java
@@ -54,8 +54,7 @@
}
rsrc.permissions().check(ChangePermission.DELETE);
- try (BatchUpdate bu =
- updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
Change.Id id = rsrc.getChange().getId();
bu.addOp(id, opFactory.create(id));
bu.execute();
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
index 0e868e70..588d56e 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
@@ -89,7 +89,7 @@
DeleteChangeMessageOp deleteChangeMessageOp =
new DeleteChangeMessageOp(resource.getChangeMessageId(), newChangeMessage);
try (BatchUpdate batchUpdate =
- updateFactory.create(resource.getChangeResource().getProject(), user, TimeUtil.nowTs())) {
+ updateFactory.create(resource.getChangeResource().getProject(), user, TimeUtil.now())) {
batchUpdate.addOp(resource.getChangeId(), deleteChangeMessageOp).execute();
}
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteComment.java b/java/com/google/gerrit/server/restapi/change/DeleteComment.java
index 044fd77..2056664 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteComment.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteComment.java
@@ -84,7 +84,7 @@
String newMessage = getCommentNewMessage(user.asIdentifiedUser().getName(), input.reason);
DeleteCommentOp deleteCommentOp = new DeleteCommentOp(rsrc, newMessage);
try (BatchUpdate batchUpdate =
- updateFactory.create(rsrc.getRevisionResource().getProject(), user, TimeUtil.nowTs())) {
+ updateFactory.create(rsrc.getRevisionResource().getProject(), user, TimeUtil.now())) {
batchUpdate.addOp(rsrc.getRevisionResource().getChange().getId(), deleteCommentOp).execute();
}
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
index 51a0b8e..7d28a39 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
@@ -54,7 +54,7 @@
public Response<CommentInfo> apply(DraftCommentResource rsrc, Input input)
throws RestApiException, UpdateException {
try (BatchUpdate bu =
- updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.now())) {
Op op = new Op(rsrc.getComment().key);
bu.addOp(rsrc.getChange().getId(), op);
bu.execute();
diff --git a/java/com/google/gerrit/server/restapi/change/DeletePrivate.java b/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
index 16b7136..08725b5 100644
--- a/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
+++ b/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
@@ -62,8 +62,7 @@
}
SetPrivateOp op = setPrivateOpFactory.create(false, input);
- try (BatchUpdate u =
- updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ try (BatchUpdate u = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
u.addOp(rsrc.getId(), op).execute();
}
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java b/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
index db8e9de..7a409e8 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
@@ -58,7 +58,7 @@
updateFactory.create(
rsrc.getChangeResource().getProject(),
rsrc.getChangeResource().getUser(),
- TimeUtil.nowTs())) {
+ TimeUtil.now())) {
bu.setNotify(getNotify(rsrc.getChange(), input));
BatchUpdateOp op;
if (rsrc.isByEmail()) {
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVote.java b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
index 7ee38d4..208cecf 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -40,6 +40,7 @@
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.AddToAttentionSetOp;
+import com.google.gerrit.server.change.AttentionSetUnchangedOp;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.ReviewerResource;
import com.google.gerrit.server.change.VoteResource;
@@ -81,7 +82,7 @@
private final RemoveReviewerControl removeReviewerControl;
private final ProjectCache projectCache;
private final MessageIdGenerator messageIdGenerator;
- private final AddToAttentionSetOp.Factory attentionSetOpfactory;
+ private final AddToAttentionSetOp.Factory attentionSetOpFactory;
private final Provider<CurrentUser> currentUserProvider;
@Inject
@@ -108,7 +109,7 @@
this.removeReviewerControl = removeReviewerControl;
this.projectCache = projectCache;
this.messageIdGenerator = messageIdGenerator;
- this.attentionSetOpfactory = attentionSetOpFactory;
+ this.attentionSetOpFactory = attentionSetOpFactory;
this.currentUserProvider = currentUserProvider;
}
@@ -133,7 +134,7 @@
try (BatchUpdate bu =
updateFactory.create(
- change.getProject(), r.getChangeResource().getUser(), TimeUtil.nowTs())) {
+ change.getProject(), r.getChangeResource().getUser(), TimeUtil.now())) {
bu.setNotify(
notifyResolver.resolve(
firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails));
@@ -146,14 +147,18 @@
r.getReviewerUser().state(),
rsrc.getLabel(),
input));
- if (!r.getReviewerUser().getAccountId().equals(currentUserProvider.get().getAccountId())) {
+ if (!input.ignoreAutomaticAttentionSetRules
+ && !r.getReviewerUser().getAccountId().equals(currentUserProvider.get().getAccountId())) {
bu.addOp(
change.getId(),
- attentionSetOpfactory.create(
+ attentionSetOpFactory.create(
r.getReviewerUser().getAccountId(),
/* reason= */ "Their vote was deleted",
/* notify= */ false));
}
+ if (input.ignoreAutomaticAttentionSetRules) {
+ bu.addOp(change.getId(), new AttentionSetUnchangedOp());
+ }
bu.execute();
}
@@ -192,9 +197,7 @@
Account.Id accountId = accountState.account().id();
- for (PatchSetApproval a :
- approvalsUtil.byPatchSetUser(
- ctx.getNotes(), psId, accountId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) {
+ for (PatchSetApproval a : approvalsUtil.byPatchSetUser(ctx.getNotes(), psId, accountId)) {
if (!labelTypes.byLabel(a.labelId()).isPresent()) {
continue; // Ignore undefined labels.
} else if (!a.label().equals(label)) {
diff --git a/java/com/google/gerrit/server/restapi/change/DraftComments.java b/java/com/google/gerrit/server/restapi/change/DraftComments.java
index ab5b9f4..a4c9400 100644
--- a/java/com/google/gerrit/server/restapi/change/DraftComments.java
+++ b/java/com/google/gerrit/server/restapi/change/DraftComments.java
@@ -75,7 +75,7 @@
}
private void checkIdentifiedUser() throws AuthException {
- if (!(user.get().isIdentifiedUser())) {
+ if (!user.get().isIdentifiedUser()) {
throw new AuthException("drafts only available to authenticated users");
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/Files.java b/java/com/google/gerrit/server/restapi/change/Files.java
index 320e57d..e996169 100644
--- a/java/com/google/gerrit/server/restapi/change/Files.java
+++ b/java/com/google/gerrit/server/restapi/change/Files.java
@@ -46,6 +46,7 @@
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.PatchListKey;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.patch.filediff.FileDiffOutput;
@@ -237,7 +238,7 @@
private Collection<String> reviewed(RevisionResource resource) throws AuthException {
CurrentUser user = self.get();
- if (!(user.isIdentifiedUser())) {
+ if (!user.isIdentifiedUser()) {
throw new AuthException("Authentication required");
}
@@ -280,11 +281,14 @@
Map<String, FileDiffOutput> oldList =
diffOperations.listModifiedFilesAgainstParent(
- project, patchSet.commitId(), /* parentNum= */ 0);
+ project, patchSet.commitId(), /* parentNum= */ 0, DiffOptions.DEFAULTS);
Map<String, FileDiffOutput> curList =
diffOperations.listModifiedFilesAgainstParent(
- project, resource.getPatchSet().commitId(), /* parentNum= */ 0);
+ project,
+ resource.getPatchSet().commitId(),
+ /* parentNum= */ 0,
+ DiffOptions.DEFAULTS);
int sz = paths.size();
List<String> pathList = Lists.newArrayListWithCapacity(sz);
diff --git a/java/com/google/gerrit/server/restapi/change/GetPatch.java b/java/com/google/gerrit/server/restapi/change/GetPatch.java
index 187ebce..dea4dc4 100644
--- a/java/com/google/gerrit/server/restapi/change/GetPatch.java
+++ b/java/com/google/gerrit/server/restapi/change/GetPatch.java
@@ -43,8 +43,6 @@
public class GetPatch implements RestReadView<RevisionResource> {
private final GitRepositoryManager repoManager;
- private static final String FILE_NOT_FOUND = "File not found: %s.";
-
@Option(name = "--zip")
private boolean zip;
@@ -118,7 +116,7 @@
};
if (path != null && bin.asString().isEmpty()) {
- throw new ResourceNotFoundException(String.format(FILE_NOT_FOUND, path));
+ throw new ResourceNotFoundException(String.format("File not found: %s.", path));
}
if (zip) {
diff --git a/java/com/google/gerrit/server/restapi/change/GetRelated.java b/java/com/google/gerrit/server/restapi/change/GetRelated.java
index 0eef468..7a1808b 100644
--- a/java/com/google/gerrit/server/restapi/change/GetRelated.java
+++ b/java/com/google/gerrit/server/restapi/change/GetRelated.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.restapi.change;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
@@ -37,7 +38,6 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.Locale;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -63,7 +63,7 @@
return Response.ok(relatedChangesInfo);
}
- public List<RelatedChangeAndCommitInfo> getRelated(RevisionResource rsrc)
+ public ImmutableList<RelatedChangeAndCommitInfo> getRelated(RevisionResource rsrc)
throws IOException, PermissionBackendException {
boolean isEdit = rsrc.getEdit().isPresent();
PatchSet basePs = isEdit ? rsrc.getEdit().get().getBasePatchSet() : rsrc.getPatchSet();
@@ -92,10 +92,10 @@
if (result.size() == 1) {
RelatedChangeAndCommitInfo r = result.get(0);
if (r.commit != null && r.commit.commit.equals(rsrc.getPatchSet().commitId().name())) {
- return Collections.emptyList();
+ return ImmutableList.of();
}
}
- return result;
+ return ImmutableList.copyOf(result);
}
static RelatedChangeAndCommitInfo newChangeAndCommit(
diff --git a/java/com/google/gerrit/server/restapi/change/Move.java b/java/com/google/gerrit/server/restapi/change/Move.java
index f52e81e..900b9e5 100644
--- a/java/com/google/gerrit/server/restapi/change/Move.java
+++ b/java/com/google/gerrit/server/restapi/change/Move.java
@@ -159,7 +159,7 @@
projectCache.get(project).orElseThrow(illegalState(project)).checkStatePermitsWrite();
Op op = new Op(input);
- try (BatchUpdate u = updateFactory.create(project, caller, TimeUtil.nowTs())) {
+ try (BatchUpdate u = updateFactory.create(project, caller, TimeUtil.now())) {
u.addOp(change.getId(), op);
u.execute();
}
@@ -257,11 +257,8 @@
* proposal: https://gerrit-review.googlesource.com/c/gerrit/+/129171
*/
private void updateApprovals(
- ChangeContext ctx, ChangeUpdate update, PatchSet.Id psId, Project.NameKey project)
- throws IOException {
- for (PatchSetApproval psa :
- approvalsUtil.byPatchSet(
- ctx.getNotes(), psId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) {
+ ChangeContext ctx, ChangeUpdate update, PatchSet.Id psId, Project.NameKey project) {
+ for (PatchSetApproval psa : approvalsUtil.byPatchSet(ctx.getNotes(), psId)) {
ProjectState projectState = projectCache.get(project).orElseThrow(illegalState(project));
Optional<LabelType> type =
projectState.getLabelTypes(ctx.getNotes()).byLabel(psa.labelId());
diff --git a/java/com/google/gerrit/server/restapi/change/PostHashtags.java b/java/com/google/gerrit/server/restapi/change/PostHashtags.java
index c1a6a13..bcaa145 100644
--- a/java/com/google/gerrit/server/restapi/change/PostHashtags.java
+++ b/java/com/google/gerrit/server/restapi/change/PostHashtags.java
@@ -48,7 +48,7 @@
req.permissions().check(ChangePermission.EDIT_HASHTAGS);
try (BatchUpdate bu =
- updateFactory.create(req.getChange().getProject(), req.getUser(), TimeUtil.nowTs())) {
+ updateFactory.create(req.getChange().getProject(), req.getUser(), TimeUtil.now())) {
SetHashtagsOp op = hashtagsFactory.create(input);
bu.addOp(req.getId(), op);
bu.execute();
diff --git a/java/com/google/gerrit/server/restapi/change/PostPrivate.java b/java/com/google/gerrit/server/restapi/change/PostPrivate.java
index f774457..45d7250 100644
--- a/java/com/google/gerrit/server/restapi/change/PostPrivate.java
+++ b/java/com/google/gerrit/server/restapi/change/PostPrivate.java
@@ -74,8 +74,7 @@
}
SetPrivateOp op = setPrivateOpFactory.create(true, input);
- try (BatchUpdate u =
- updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ try (BatchUpdate u = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
u.addOp(rsrc.getId(), op).execute();
}
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 5c252f4..6a89247 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -137,6 +137,7 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.sql.Timestamp;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -274,10 +275,10 @@
public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input)
throws RestApiException, UpdateException, IOException, PermissionBackendException,
ConfigInvalidException, PatchListNotAvailableException {
- return apply(revision, input, TimeUtil.nowTs());
+ return apply(revision, input, TimeUtil.now());
}
- public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input, Timestamp ts)
+ public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input, Instant ts)
throws RestApiException, UpdateException, IOException, PermissionBackendException,
ConfigInvalidException, PatchListNotAvailableException {
// Respect timestamp, but truncate at change created-on time.
@@ -530,7 +531,7 @@
ChangeData cd,
PatchSet patchSet,
List<ReviewerModification> reviewerModifications,
- Timestamp when) {
+ Instant when) {
List<AccountState> newlyAddedReviewers = new ArrayList<>();
// There are no events for CCs and reviewers added/deleted by email.
@@ -1203,7 +1204,7 @@
parent);
} else {
// In ChangeUpdate#putComment() the draft with the same ID will be deleted.
- comment.writtenOn = ctx.getWhen();
+ comment.writtenOn = Timestamp.from(ctx.getWhen());
comment.side = inputComment.side();
comment.message = inputComment.message;
}
@@ -1230,7 +1231,9 @@
throws CommentsRejectedException {
CommentValidationContext commentValidationCtx =
CommentValidationContext.create(
- ctx.getChange().getChangeId(), ctx.getChange().getProject().get());
+ ctx.getChange().getChangeId(),
+ ctx.getChange().getProject().get(),
+ ctx.getChange().getDest().branch());
String changeMessage = Strings.nullToEmpty(in.message).trim();
ImmutableList<CommentForValidation> draftsForValidation =
Stream.concat(
@@ -1309,17 +1312,17 @@
return robotComment;
}
- private List<FixSuggestion> createFixSuggestionsFromInput(
+ private ImmutableList<FixSuggestion> createFixSuggestionsFromInput(
List<FixSuggestionInfo> fixSuggestionInfos) {
if (fixSuggestionInfos == null) {
- return Collections.emptyList();
+ return ImmutableList.of();
}
List<FixSuggestion> fixSuggestions = new ArrayList<>(fixSuggestionInfos.size());
for (FixSuggestionInfo fixSuggestionInfo : fixSuggestionInfos) {
fixSuggestions.add(createFixSuggestionFromInput(fixSuggestionInfo));
}
- return fixSuggestions;
+ return ImmutableList.copyOf(fixSuggestions);
}
private FixSuggestion createFixSuggestionFromInput(FixSuggestionInfo fixSuggestionInfo) {
@@ -1406,7 +1409,7 @@
}
private boolean updateLabels(ProjectState projectState, ChangeContext ctx)
- throws ResourceConflictException, IOException {
+ throws ResourceConflictException {
Map<String, Short> inLabels = firstNonNull(in.labels, Collections.emptyMap());
// If no labels were modified and change is closed, abort early.
@@ -1573,18 +1576,12 @@
}
private Map<String, PatchSetApproval> scanLabels(
- ProjectState projectState, ChangeContext ctx, List<PatchSetApproval> del)
- throws IOException {
+ ProjectState projectState, ChangeContext ctx, List<PatchSetApproval> del) {
LabelTypes labelTypes = projectState.getLabelTypes(ctx.getNotes());
Map<String, PatchSetApproval> current = new HashMap<>();
for (PatchSetApproval a :
- approvalsUtil.byPatchSetUser(
- ctx.getNotes(),
- psId,
- user.getAccountId(),
- ctx.getRevWalk(),
- ctx.getRepoView().getConfig())) {
+ approvalsUtil.byPatchSetUser(ctx.getNotes(), psId, user.getAccountId())) {
if (a.isLegacySubmit()) {
continue;
}
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewers.java b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
index 4691550..9bc80a4 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
@@ -70,8 +70,7 @@
if (modification.op == null) {
return Response.ok(modification.result);
}
- try (BatchUpdate bu =
- updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
bu.setNotify(resolveNotify(rsrc, input));
Change.Id id = rsrc.getChange().getId();
bu.addOp(id, modification.op);
diff --git a/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java b/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java
deleted file mode 100644
index 4acf809..0000000
--- a/java/com/google/gerrit/server/restapi/change/PreviewSubmit.java
+++ /dev/null
@@ -1,187 +0,0 @@
-// 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.server.restapi.change;
-
-import com.google.common.base.Strings;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.extensions.api.changes.SubmitInput;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.BinaryResult;
-import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
-import com.google.gerrit.extensions.restapi.PreconditionFailedException;
-import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.change.ArchiveFormatInternal;
-import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.ioutil.LimitedByteArrayOutputStream;
-import com.google.gerrit.server.ioutil.LimitedByteArrayOutputStream.LimitExceededException;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.submit.MergeOp;
-import com.google.gerrit.server.submit.MergeOpRepoManager;
-import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
-import com.google.gerrit.server.update.UpdateException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.Collection;
-import org.apache.commons.compress.archivers.ArchiveOutputStream;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.storage.pack.PackConfig;
-import org.eclipse.jgit.transport.BundleWriter;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.kohsuke.args4j.Option;
-
-public class PreviewSubmit implements RestReadView<RevisionResource> {
- private static final int MAX_DEFAULT_BUNDLE_SIZE = 100 * 1024 * 1024;
-
- private final Provider<MergeOp> mergeOpProvider;
- private final AllowedFormats allowedFormats;
- private int maxBundleSize;
- private String format;
-
- @Option(name = "--format")
- public void setFormat(String f) {
- this.format = f;
- }
-
- @Inject
- PreviewSubmit(
- Provider<MergeOp> mergeOpProvider,
- AllowedFormats allowedFormats,
- @GerritServerConfig Config cfg) {
- this.mergeOpProvider = mergeOpProvider;
- this.allowedFormats = allowedFormats;
- this.maxBundleSize = cfg.getInt("download", "maxBundleSize", MAX_DEFAULT_BUNDLE_SIZE);
- }
-
- @Override
- public Response<BinaryResult> apply(RevisionResource rsrc)
- throws RestApiException, UpdateException, IOException, ConfigInvalidException,
- PermissionBackendException {
- if (Strings.isNullOrEmpty(format)) {
- throw new BadRequestException("format is not specified");
- }
- ArchiveFormatInternal f = allowedFormats.extensions.get("." + format);
- if (f == null && format.equals("tgz")) {
- // Always allow tgz, even when the allowedFormats doesn't contain it.
- // Then we allow at least one format even if the list of allowed
- // formats is empty.
- f = ArchiveFormatInternal.TGZ;
- }
- if (f == null) {
- throw new BadRequestException("unknown archive format");
- }
-
- Change change = rsrc.getChange();
- if (!change.isNew()) {
- throw new PreconditionFailedException("change is " + ChangeUtil.status(change));
- }
- if (!rsrc.getUser().isIdentifiedUser()) {
- throw new MethodNotAllowedException("Anonymous users cannot submit");
- }
-
- return Response.ok(getBundles(rsrc, f));
- }
-
- private BinaryResult getBundles(RevisionResource rsrc, ArchiveFormatInternal f)
- throws RestApiException, UpdateException, IOException, ConfigInvalidException,
- PermissionBackendException {
- IdentifiedUser caller = rsrc.getUser().asIdentifiedUser();
- Change change = rsrc.getChange();
-
- @SuppressWarnings("resource") // Returned BinaryResult takes ownership and handles closing.
- MergeOp op = mergeOpProvider.get();
- try {
- op.merge(change, caller, false, new SubmitInput(), true);
- BinaryResult bin = new SubmitPreviewResult(op, f, maxBundleSize);
- bin.disableGzip()
- .setContentType(f.getMimeType())
- .setAttachmentName("submit-preview-" + change.getChangeId() + "." + format);
- return bin;
- } catch (RestApiException
- | UpdateException
- | IOException
- | ConfigInvalidException
- | RuntimeException
- | PermissionBackendException e) {
- op.close();
- throw e;
- }
- }
-
- private static class SubmitPreviewResult extends BinaryResult {
-
- private final MergeOp mergeOp;
- private final ArchiveFormatInternal archiveFormat;
- private final int maxBundleSize;
-
- private SubmitPreviewResult(
- MergeOp mergeOp, ArchiveFormatInternal archiveFormat, int maxBundleSize) {
- this.mergeOp = mergeOp;
- this.archiveFormat = archiveFormat;
- this.maxBundleSize = maxBundleSize;
- }
-
- @Override
- public void writeTo(OutputStream out) throws IOException {
- try (ArchiveOutputStream aos = archiveFormat.createArchiveOutputStream(out)) {
- MergeOpRepoManager orm = mergeOp.getMergeOpRepoManager();
- for (Project.NameKey p : mergeOp.getAllProjects()) {
- OpenRepo or = orm.getRepo(p);
- BundleWriter bw = new BundleWriter(or.getCodeReviewRevWalk().getObjectReader());
- bw.setObjectCountCallback(null);
- bw.setPackConfig(new PackConfig(or.getRepo()));
- Collection<ReceiveCommand> refs = or.getUpdate().getRefUpdates().values();
- for (ReceiveCommand r : refs) {
- bw.include(r.getRefName(), r.getNewId());
- ObjectId oldId = r.getOldId();
- if (!oldId.equals(ObjectId.zeroId())
- // Probably the client doesn't already have NoteDb data.
- && !RefNames.isNoteDbMetaRef(r.getRefName())) {
- bw.assume(or.getCodeReviewRevWalk().parseCommit(oldId));
- }
- }
- LimitedByteArrayOutputStream bos = new LimitedByteArrayOutputStream(maxBundleSize, 1024);
- bw.writeBundle(NullProgressMonitor.INSTANCE, bos);
- // This naming scheme cannot produce directory/file conflicts
- // as no projects contains ".git/":
- String path = p.get() + ".git";
- archiveFormat.putEntry(aos, path, bos.toByteArray());
- }
- } catch (LimitExceededException e) {
- throw new NotImplementedException("The bundle is too big to generate at the server", e);
- } catch (NoSuchProjectException e) {
- throw new IOException(e);
- }
- }
-
- @Override
- public void close() throws IOException {
- mergeOp.close();
- }
- }
-}
diff --git a/java/com/google/gerrit/server/restapi/change/PutAssignee.java b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
index dcf616c..d41620e 100644
--- a/java/com/google/gerrit/server/restapi/change/PutAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
@@ -98,7 +98,7 @@
}
try (BatchUpdate bu =
- updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.now())) {
SetAssigneeOp op = assigneeFactory.create(assignee);
bu.addOp(rsrc.getId(), op);
diff --git a/java/com/google/gerrit/server/restapi/change/PutDescription.java b/java/com/google/gerrit/server/restapi/change/PutDescription.java
index 7c54074..5b5bc15 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDescription.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDescription.java
@@ -57,7 +57,7 @@
Op op = new Op(input != null ? input : new DescriptionInput(), rsrc.getPatchSet().id());
try (BatchUpdate u =
- updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.now())) {
u.addOp(rsrc.getChange().getId(), op);
u.execute();
}
diff --git a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
index 84a3d89..6411087 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
@@ -40,7 +40,7 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Collections;
import java.util.Optional;
@@ -87,7 +87,7 @@
String.format("Invalid inReplyTo, comment %s not found", in.inReplyTo));
}
try (BatchUpdate bu =
- updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.now())) {
Op op = new Op(rsrc.getComment().key, in);
bu.addOp(rsrc.getChange().getId(), op);
bu.execute();
@@ -145,7 +145,7 @@
}
}
- private static HumanComment update(HumanComment e, DraftInput in, Timestamp when) {
+ private static HumanComment update(HumanComment e, DraftInput in, Instant when) {
if (in.side != null) {
e.side = in.side();
}
@@ -154,7 +154,7 @@
}
e.setLineNbrAndRange(in.line, in.range);
e.message = in.message.trim();
- e.writtenOn = when;
+ e.setWrittenOn(when);
if (in.tag != null) {
// TODO(dborowitz): Can we support changing tags via PUT?
e.tag = in.tag;
diff --git a/java/com/google/gerrit/server/restapi/change/PutMessage.java b/java/com/google/gerrit/server/restapi/change/PutMessage.java
index 1ed7fd7..c62200a 100644
--- a/java/com/google/gerrit/server/restapi/change/PutMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/PutMessage.java
@@ -47,7 +47,7 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.TimeZone;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
@@ -126,7 +126,7 @@
throw new ResourceConflictException("new and existing commit message are the same");
}
- Timestamp ts = TimeUtil.nowTs();
+ Instant ts = TimeUtil.now();
try (BatchUpdate bu =
updateFactory.create(resource.getChange().getProject(), userProvider.get(), ts)) {
// Ensure that BatchUpdate will update the same repo
@@ -161,7 +161,7 @@
ObjectInserter objectInserter,
RevCommit basePatchSetCommit,
String commitMessage,
- Timestamp timestamp)
+ Instant timestamp)
throws IOException {
CommitBuilder builder = new CommitBuilder();
builder.setTreeId(basePatchSetCommit.getTree());
diff --git a/java/com/google/gerrit/server/restapi/change/PutTopic.java b/java/com/google/gerrit/server/restapi/change/PutTopic.java
index 3031781..c9b436e 100644
--- a/java/com/google/gerrit/server/restapi/change/PutTopic.java
+++ b/java/com/google/gerrit/server/restapi/change/PutTopic.java
@@ -63,7 +63,7 @@
SetTopicOp op = topicOpFactory.create(sanitizedInput.topic);
try (BatchUpdate u =
- updateFactory.create(req.getChange().getProject(), req.getUser(), TimeUtil.nowTs())) {
+ updateFactory.create(req.getChange().getProject(), req.getUser(), TimeUtil.now())) {
u.addOp(req.getId(), op);
u.execute();
}
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index 2077fb8..1a0f2b6 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -114,7 +114,7 @@
ObjectReader reader = oi.newReader();
RevWalk rw = CodeReviewCommit.newRevWalk(reader);
BatchUpdate bu =
- updateFactory.create(change.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ updateFactory.create(change.getProject(), rsrc.getUser(), TimeUtil.now())) {
if (!change.isNew()) {
throw new ResourceConflictException("change is " + ChangeUtil.status(change));
} else if (!hasOneParent(rw, rsrc.getPatchSet())) {
diff --git a/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java b/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java
index 7fe463e..bd3e8ec 100644
--- a/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java
+++ b/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java
@@ -81,7 +81,7 @@
ChangeResource changeResource = attentionResource.getChangeResource();
try (BatchUpdate bu =
updateFactory.create(
- changeResource.getProject(), changeResource.getUser(), TimeUtil.nowTs())) {
+ changeResource.getProject(), changeResource.getUser(), TimeUtil.now())) {
RemoveFromAttentionSetOp op =
opFactory.create(attentionResource.getAccountId(), input.reason, true);
bu.addOp(changeResource.getId(), op);
diff --git a/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java b/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
index 53d0f18..49286fc 100644
--- a/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
+++ b/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
@@ -151,7 +151,7 @@
commentsUtil.newHumanComment(
changeNotes,
currentUser,
- TimeUtil.nowTs(),
+ TimeUtil.now(),
commentInput.path,
commentInput.patchSet == null
? changeNotes.getChange().currentPatchSetId()
@@ -327,7 +327,7 @@
// message here, then it would be possible to probe whether an account exists.
} catch (AuthException ex) {
// adding users without permission to the attention set should fail silently.
- logger.atFine().log(ex.getMessage());
+ logger.atFine().log("%s", ex.getMessage());
}
}
@@ -352,7 +352,7 @@
// message here, then it would be possible to probe whether an account exists.
} catch (AuthException ex) {
// this should never happen since removing users with permissions should work.
- logger.atSevere().log(ex.getMessage());
+ logger.atSevere().log("%s", ex.getMessage());
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/Restore.java b/java/com/google/gerrit/server/restapi/change/Restore.java
index b2d1d3a..19d0677 100644
--- a/java/com/google/gerrit/server/restapi/change/Restore.java
+++ b/java/com/google/gerrit/server/restapi/change/Restore.java
@@ -100,7 +100,7 @@
Op op = new Op(input);
try (BatchUpdate u =
- updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.now())) {
u.addOp(rsrc.getId(), op).execute();
}
return Response.ok(json.noOptions().format(op.change));
diff --git a/java/com/google/gerrit/server/restapi/change/Revert.java b/java/com/google/gerrit/server/restapi/change/Revert.java
index 8d48c88..7dd3e7a 100644
--- a/java/com/google/gerrit/server/restapi/change/Revert.java
+++ b/java/com/google/gerrit/server/restapi/change/Revert.java
@@ -47,7 +47,6 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
@@ -99,12 +98,11 @@
if (patch == null) {
throw new ResourceNotFoundException(changeIdToRevert.toString());
}
- Timestamp timestamp = TimeUtil.nowTs();
return Response.ok(
json.noOptions()
.format(
rsrc.getProject(),
- commitUtil.createRevertChange(notes, rsrc.getUser(), input, timestamp)));
+ commitUtil.createRevertChange(notes, rsrc.getUser(), input, TimeUtil.now())));
}
@Override
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
index 8bde6e7..383eda0 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -80,8 +80,8 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
-import java.sql.Timestamp;
import java.text.MessageFormat;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -251,7 +251,7 @@
Multimap<BranchNameKey, ChangeData> changesPerProjectAndBranch = ArrayListMultimap.create();
changeData.stream().forEach(c -> changesPerProjectAndBranch.put(c.change().getDest(), c));
cherryPickInput = createCherryPickInput(revertInput);
- Timestamp timestamp = TimeUtil.nowTs();
+ Instant timestamp = TimeUtil.now();
for (BranchNameKey projectAndBranch : changesPerProjectAndBranch.keySet()) {
cherryPickInput.base = null;
@@ -290,7 +290,7 @@
Project.NameKey project,
Iterator<PatchSetData> sortedChangesInProjectAndBranch,
Set<ObjectId> commitIdsInProjectAndBranch,
- Timestamp timestamp)
+ Instant timestamp)
throws IOException, RestApiException, UpdateException, ConfigInvalidException,
PermissionBackendException {
@@ -314,10 +314,7 @@
}
private void createCherryPickedRevert(
- RevertInput revertInput,
- Project.NameKey project,
- ChangeNotes changeNotes,
- Timestamp timestamp)
+ RevertInput revertInput, Project.NameKey project, ChangeNotes changeNotes, Instant timestamp)
throws IOException, ConfigInvalidException, UpdateException, RestApiException {
ObjectId revCommitId =
commitUtil.createRevertCommit(revertInput.message, changeNotes, user.get(), timestamp);
@@ -326,7 +323,7 @@
cherryPickInput.message = revertInput.message;
ObjectId generatedChangeId = CommitMessageUtil.generateChangeId();
Change.Id cherryPickRevertChangeId = Change.id(seq.nextChangeId());
- try (BatchUpdate bu = updateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = updateFactory.create(project, user.get(), TimeUtil.now())) {
bu.setNotify(
notifyResolver.resolve(
firstNonNull(cherryPickInput.notify, NotifyHandling.ALL),
@@ -349,7 +346,7 @@
}
private void craeteNormalRevert(
- RevertInput revertInput, ChangeNotes changeNotes, Timestamp timestamp)
+ RevertInput revertInput, ChangeNotes changeNotes, Instant timestamp)
throws IOException, RestApiException, UpdateException, ConfigInvalidException {
Change.Id revertId =
@@ -558,14 +555,14 @@
private final ObjectId revCommitId;
private final ObjectId computedChangeId;
private final Change.Id cherryPickRevertChangeId;
- private final Timestamp timestamp;
+ private final Instant timestamp;
private final boolean workInProgress;
CreateCherryPickOp(
ObjectId revCommitId,
ObjectId computedChangeId,
Change.Id cherryPickRevertChangeId,
- Timestamp timestamp,
+ Instant timestamp,
Boolean workInProgress) {
this.revCommitId = revCommitId;
this.computedChangeId = computedChangeId;
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
index 7f7c1ad..cc81aac 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
@@ -18,7 +18,6 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
@@ -44,6 +43,7 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
@@ -227,7 +227,7 @@
} catch (QueryParseException e) {
// Unhandled, because owner:self will never provoke a QueryParseException
logger.atSevere().withCause(e).log("Exception while suggesting reviewers");
- return ImmutableMap.of();
+ return new HashMap<>();
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index d6c4c51..e3cf4db 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -413,7 +413,7 @@
GroupAsReviewer result = new GroupAsReviewer();
int maxAllowed = suggestReviewers.getMaxAllowed();
int maxAllowedWithoutConfirmation = suggestReviewers.getMaxAllowedWithoutConfirmation();
- logger.atFine().log("maxAllowedWithoutConfirmation: " + maxAllowedWithoutConfirmation);
+ logger.atFine().log("maxAllowedWithoutConfirmation: %s", maxAllowedWithoutConfirmation);
if (!ReviewerModifier.isLegalReviewerGroup(group.getUUID())) {
logger.atFine().log("Ignore group %s that is not legal as reviewer", group.getUUID());
diff --git a/java/com/google/gerrit/server/restapi/change/Revisions.java b/java/com/google/gerrit/server/restapi/change/Revisions.java
index 69b82ba..41fecaf 100644
--- a/java/com/google/gerrit/server/restapi/change/Revisions.java
+++ b/java/com/google/gerrit/server/restapi/change/Revisions.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.restapi.change;
import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.PatchSet;
@@ -38,9 +39,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.eclipse.jgit.lib.ObjectId;
@@ -121,7 +120,7 @@
}
}
- private List<RevisionResource> find(ChangeResource change, String id)
+ private ImmutableList<RevisionResource> find(ChangeResource change, String id)
throws IOException, AuthException {
if (id.equals("0") || id.equals("edit")) {
return loadEdit(change, null);
@@ -131,7 +130,7 @@
} else if (id.length() < 4 || id.length() > ObjectIds.STR_LEN) {
// Require a minimum of 4 digits.
// Impossibly long identifier will never match.
- return Collections.emptyList();
+ return ImmutableList.of();
} else {
List<RevisionResource> out = new ArrayList<>();
for (PatchSet ps : psUtil.byChange(change.getNotes())) {
@@ -143,20 +142,23 @@
if (out.isEmpty() && ObjectId.isId(id)) {
return loadEdit(change, ObjectId.fromString(id));
}
- return out;
+ return ImmutableList.copyOf(out);
}
}
- private List<RevisionResource> byLegacyPatchSetId(ChangeResource change, String id) {
+ private ImmutableList<RevisionResource> byLegacyPatchSetId(ChangeResource change, String id) {
PatchSet ps = psUtil.get(change.getNotes(), PatchSet.id(change.getId(), Integer.parseInt(id)));
if (ps != null) {
- return Collections.singletonList(new RevisionResource(change, ps));
+ return ImmutableList.of(new RevisionResource(change, ps));
}
- return Collections.emptyList();
+ return ImmutableList.of();
}
- private List<RevisionResource> loadEdit(ChangeResource change, @Nullable ObjectId commitId)
- throws AuthException, IOException {
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
+ private ImmutableList<RevisionResource> loadEdit(
+ ChangeResource change, @Nullable ObjectId commitId) throws AuthException, IOException {
Optional<ChangeEdit> edit = editUtil.byChange(change.getNotes(), change.getUser());
if (edit.isPresent()) {
RevCommit editCommit = edit.get().getEditCommit();
@@ -165,12 +167,12 @@
.id(PatchSet.id(change.getId(), 0))
.commitId(editCommit)
.uploader(change.getUser().getAccountId())
- .createdOn(new Timestamp(editCommit.getCommitterIdent().getWhen().getTime()))
+ .createdOn(editCommit.getCommitterIdent().getWhen().toInstant())
.build();
if (commitId == null || editCommit.equals(commitId)) {
- return Collections.singletonList(new RevisionResource(change, ps, edit));
+ return ImmutableList.of(new RevisionResource(change, ps, edit));
}
}
- return Collections.emptyList();
+ return ImmutableList.of();
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
index c118766..9f019b6 100644
--- a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
+++ b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
@@ -63,8 +63,7 @@
throw new ResourceConflictException("change is not work in progress");
}
- try (BatchUpdate bu =
- updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ 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));
bu.execute();
diff --git a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
index fdaad9d..0ad5180 100644
--- a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
+++ b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
@@ -63,8 +63,7 @@
throw new ResourceConflictException("change is already work in progress");
}
- try (BatchUpdate bu =
- updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
bu.setNotify(NotifyResolver.Result.create(firstNonNull(input.notify, NotifyHandling.NONE)));
bu.addOp(rsrc.getChange().getId(), opFactory.create(true, input));
bu.execute();
diff --git a/java/com/google/gerrit/server/restapi/change/Submit.java b/java/com/google/gerrit/server/restapi/change/Submit.java
index 876c92c..c2f9b85 100644
--- a/java/com/google/gerrit/server/restapi/change/Submit.java
+++ b/java/com/google/gerrit/server/restapi/change/Submit.java
@@ -257,7 +257,7 @@
return "Change " + c.getId() + " is marked work in progress";
}
try {
- MergeOp.checkSubmitRule(c, false);
+ MergeOp.checkSubmitRequirements(c);
} catch (ResourceConflictException e) {
return "Change " + c.getId() + " is not ready: " + e.getMessage();
}
@@ -299,12 +299,15 @@
ChangeData cd = resource.getChangeResource().getChangeData();
try {
- MergeOp.checkSubmitRule(cd, false);
+ MergeOp.checkSubmitRequirements(cd);
} catch (ResourceConflictException e) {
return null; // submit not visible
}
- ChangeSet cs = mergeSuperSet.get().completeChangeSet(cd.change(), resource.getUser());
+ ChangeSet cs =
+ mergeSuperSet
+ .get()
+ .completeChangeSet(cd.change(), resource.getUser(), /*includingTopicClosure= */ false);
String topic = change.getTopic();
int topicSize = 0;
if (!Strings.isNullOrEmpty(topic)) {
diff --git a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
index 214a001..74ddae1 100644
--- a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
+++ b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
@@ -14,10 +14,12 @@
package com.google.gerrit.server.restapi.change;
+import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.NON_VISIBLE_CHANGES;
+import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.TOPIC_CLOSURE;
import static java.util.Collections.reverseOrder;
-import static java.util.stream.Collectors.toList;
+import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Change;
import com.google.gerrit.exceptions.StorageException;
@@ -127,7 +129,10 @@
int hidden;
if (c.isNew()) {
- ChangeSet cs = mergeSuperSet.get().completeChangeSet(c, resource.getUser());
+ ChangeSet cs =
+ mergeSuperSet
+ .get()
+ .completeChangeSet(c, resource.getUser(), options.contains(TOPIC_CLOSURE));
cds = ensureRequiredDataIsLoaded(cs.changes().asList());
hidden = cs.nonVisibleChanges().size();
} else if (c.isMerged()) {
@@ -153,11 +158,11 @@
}
}
- private List<ChangeData> sort(List<ChangeData> cds, int hidden) throws IOException {
+ private ImmutableList<ChangeData> sort(List<ChangeData> cds, int hidden) throws IOException {
if (cds.size() <= 1 && hidden == 0) {
// Skip sorting for singleton lists, to avoid WalkSorter opening the
// repo just to fill out the commit field in PatchSetData.
- return Collections.emptyList();
+ return ImmutableList.of();
}
long numProjectsDistinct = cds.stream().map(ChangeData::project).distinct().count();
@@ -167,7 +172,7 @@
// We either have only a single change per project which means that WalkSorter won't make a
// difference compared to our index-backed sort, or we are looking at more than 5 projects
// which would make WalkSorter too expensive for this call.
- return cds.stream().sorted(COMPARATOR).collect(toList());
+ return cds.stream().sorted(COMPARATOR).collect(toImmutableList());
}
// Perform more expensive walk-sort.
@@ -175,7 +180,7 @@
for (PatchSetData psd : sorter.get().sort(cds)) {
sorted.add(psd.data());
}
- return sorted;
+ return ImmutableList.copyOf(sorted);
}
private static List<ChangeData> ensureRequiredDataIsLoaded(List<ChangeData> cds) {
diff --git a/java/com/google/gerrit/server/restapi/config/ListTasks.java b/java/com/google/gerrit/server/restapi/config/ListTasks.java
index eac9653..dcc44ae 100644
--- a/java/com/google/gerrit/server/restapi/config/ListTasks.java
+++ b/java/com/google/gerrit/server/restapi/config/ListTasks.java
@@ -129,7 +129,7 @@
public TaskInfo(Task<?> task) {
this.id = HexFormat.fromInt(task.getTaskId());
this.state = task.getState();
- this.startTime = new Timestamp(task.getStartTime().getTime());
+ this.startTime = Timestamp.from(task.getStartTime());
this.delay = task.getDelay(TimeUnit.MILLISECONDS);
this.command = task.toString();
this.queueName = task.getQueueName();
diff --git a/java/com/google/gerrit/server/restapi/group/CreateGroup.java b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
index ee86010..f257f86 100644
--- a/java/com/google/gerrit/server/restapi/group/CreateGroup.java
+++ b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
@@ -212,7 +212,7 @@
createGroupArgs.uuid,
GroupUuid.make(
createGroupArgs.getGroupName(),
- self.get().newCommitterIdent(TimeUtil.nowTs(), serverTimeZone)));
+ self.get().newCommitterIdent(TimeUtil.now(), serverTimeZone)));
InternalGroupCreation groupCreation =
InternalGroupCreation.builder()
.setGroupUUID(uuid)
diff --git a/java/com/google/gerrit/server/restapi/group/GetAuditLog.java b/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
index e3aa0f3..1b0fcd4 100644
--- a/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
+++ b/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
@@ -102,7 +102,7 @@
auditEvents.add(
GroupAuditEventInfo.createRemoveUserEvent(
accountLoader.get(auditEvent.removedBy().orElse(null)),
- auditEvent.removedOn(),
+ auditEvent.removedOn().orElse(null),
member));
}
}
@@ -134,7 +134,7 @@
auditEvents.add(
GroupAuditEventInfo.createRemoveGroupEvent(
accountLoader.get(auditEvent.removedBy().orElse(null)),
- auditEvent.removedOn(),
+ auditEvent.removedOn().orElse(null),
member));
}
}
diff --git a/java/com/google/gerrit/server/restapi/group/GroupJson.java b/java/com/google/gerrit/server/restapi/group/GroupJson.java
index e1459c3..6d3fa01 100644
--- a/java/com/google/gerrit/server/restapi/group/GroupJson.java
+++ b/java/com/google/gerrit/server/restapi/group/GroupJson.java
@@ -121,7 +121,7 @@
}
}
- info.createdOn = internalGroup.getCreatedOn();
+ info.setCreatedOn(internalGroup.getCreatedOn());
if (options.contains(MEMBERS)) {
info.members = listMembers.get().getDirectMembers(internalGroup, groupControlSupplier.get());
diff --git a/java/com/google/gerrit/server/restapi/group/GroupsCollection.java b/java/com/google/gerrit/server/restapi/group/GroupsCollection.java
index 08cc974..ac81f54 100644
--- a/java/com/google/gerrit/server/restapi/group/GroupsCollection.java
+++ b/java/com/google/gerrit/server/restapi/group/GroupsCollection.java
@@ -70,7 +70,7 @@
final CurrentUser user = self.get();
if (user instanceof AnonymousUser) {
throw new AuthException("Authentication required");
- } else if (!(user.isIdentifiedUser())) {
+ } else if (!user.isIdentifiedUser()) {
throw new ResourceNotFoundException();
}
diff --git a/java/com/google/gerrit/server/restapi/group/ListGroups.java b/java/com/google/gerrit/server/restapi/group/ListGroups.java
index 854f091..b94e44d 100644
--- a/java/com/google/gerrit/server/restapi/group/ListGroups.java
+++ b/java/com/google/gerrit/server/restapi/group/ListGroups.java
@@ -57,8 +57,8 @@
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.NavigableMap;
import java.util.Set;
-import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.Predicate;
import java.util.regex.Pattern;
@@ -235,8 +235,9 @@
}
@Override
- public Response<SortedMap<String, GroupInfo>> apply(TopLevelResource resource) throws Exception {
- SortedMap<String, GroupInfo> output = new TreeMap<>();
+ public Response<NavigableMap<String, GroupInfo>> apply(TopLevelResource resource)
+ throws Exception {
+ NavigableMap<String, GroupInfo> output = new TreeMap<>();
for (GroupInfo info : get()) {
output.put(MoreObjects.firstNonNull(info.name, "Group " + Url.decode(info.id)), info);
info.name = null;
diff --git a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
index 92038b0..8a0cc39 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
@@ -16,6 +16,7 @@
import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.entities.AccessSection;
import com.google.gerrit.entities.Change;
@@ -49,7 +50,6 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.List;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
@@ -113,8 +113,8 @@
.checkStatePermitsWrite();
MetaDataUpdate.User metaDataUpdateUser = metaDataUpdateFactory.get();
- List<AccessSection> removals = setAccess.getAccessSections(input.remove);
- List<AccessSection> additions = setAccess.getAccessSections(input.add);
+ ImmutableList<AccessSection> removals = setAccess.getAccessSections(input.remove);
+ ImmutableList<AccessSection> additions = setAccess.getAccessSections(input.add);
Project.NameKey newParentProjectName =
input.parent == null ? null : Project.nameKey(input.parent);
@@ -152,7 +152,7 @@
ObjectReader objReader = objInserter.newReader();
RevWalk rw = new RevWalk(objReader);
BatchUpdate bu =
- updateFactory.create(rsrc.getNameKey(), rsrc.getUser(), TimeUtil.nowTs())) {
+ updateFactory.create(rsrc.getNameKey(), rsrc.getUser(), TimeUtil.now())) {
bu.setRepository(md.getRepository(), rw, objInserter);
ChangeInserter ins = newInserter(changeId, commit);
bu.insertChange(ins);
diff --git a/java/com/google/gerrit/server/restapi/project/CreateLabel.java b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
index e3f293a..01686ff 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateLabel.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
@@ -43,6 +43,7 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.List;
+import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
@@ -148,6 +149,11 @@
List<LabelValue> values = LabelDefinitionInputParser.parseValues(input.values);
LabelType.Builder labelType = LabelType.builder(LabelType.checkName(label), values);
+ if (input.description != null) {
+ String description = Strings.emptyToNull(input.description.trim());
+ labelType.setDescription(Optional.ofNullable(description));
+ }
+
if (input.function != null && !input.function.trim().isEmpty()) {
labelType.setFunction(LabelDefinitionInputParser.parseFunction(input.function));
} else {
diff --git a/java/com/google/gerrit/server/restapi/project/CreateProject.java b/java/com/google/gerrit/server/restapi/project/CreateProject.java
index f3b2bad..8203346 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateProject.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateProject.java
@@ -18,6 +18,7 @@
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.entities.RefNames;
@@ -57,7 +58,6 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.Lock;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -194,7 +194,8 @@
}
}
- private List<String> normalizeBranchNames(List<String> branches) throws BadRequestException {
+ private ImmutableList<String> normalizeBranchNames(List<String> branches)
+ throws BadRequestException {
if (branches == null || branches.isEmpty()) {
// Use host-level default for HEAD or fall back to 'master' if nothing else was specified in
// the input.
@@ -203,7 +204,7 @@
defaultBranch != null
? normalizeAndValidateBranch(defaultBranch)
: Constants.R_HEADS + Constants.MASTER;
- return Collections.singletonList(defaultBranch);
+ return ImmutableList.of(defaultBranch);
}
List<String> normalizedBranches = new ArrayList<>();
for (String branch : branches) {
@@ -212,7 +213,7 @@
normalizedBranches.add(branch);
}
}
- return normalizedBranches;
+ return ImmutableList.copyOf(normalizedBranches);
}
private String normalizeAndValidateBranch(String branch) throws BadRequestException {
diff --git a/java/com/google/gerrit/server/restapi/project/CreateTag.java b/java/com/google/gerrit/server/restapi/project/CreateTag.java
index b552ff5..6980006 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateTag.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateTag.java
@@ -136,7 +136,7 @@
resource
.getUser()
.asIdentifiedUser()
- .newCommitterIdent(TimeUtil.nowTs(), TimeZone.getDefault()));
+ .newCommitterIdent(TimeUtil.now(), TimeZone.getDefault()));
}
Ref result = tag.call();
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteRef.java b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
index 60405a6..33e6613 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteRef.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
@@ -269,7 +269,7 @@
msg = format("Cannot delete %s: %s", cmd.getRefName(), cmd.getResult());
break;
}
- logger.atSevere().log(msg);
+ logger.atSevere().log("%s", msg);
errorMessages.append(msg);
errorMessages.append("\n");
}
diff --git a/java/com/google/gerrit/server/restapi/project/GetReflog.java b/java/com/google/gerrit/server/restapi/project/GetReflog.java
index f9c6fd9..e0131ee 100644
--- a/java/com/google/gerrit/server/restapi/project/GetReflog.java
+++ b/java/com/google/gerrit/server/restapi/project/GetReflog.java
@@ -23,7 +23,7 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.server.CommonConverters;
-import com.google.gerrit.server.args4j.TimestampHandler;
+import com.google.gerrit.server.args4j.InstantHandler;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -31,7 +31,7 @@
import com.google.gerrit.server.project.BranchResource;
import com.google.inject.Inject;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jgit.lib.ReflogEntry;
@@ -60,9 +60,9 @@
metaVar = "TIMESTAMP",
usage =
"timestamp from which the reflog entries should be listed (UTC, format: "
- + TimestampHandler.TIMESTAMP_FORMAT
+ + InstantHandler.TIMESTAMP_FORMAT
+ ")")
- public GetReflog setFrom(Timestamp from) {
+ public GetReflog setFrom(Instant from) {
this.from = from;
return this;
}
@@ -72,16 +72,16 @@
metaVar = "TIMESTAMP",
usage =
"timestamp until which the reflog entries should be listed (UTC, format: "
- + TimestampHandler.TIMESTAMP_FORMAT
+ + InstantHandler.TIMESTAMP_FORMAT
+ ")")
- public GetReflog setTo(Timestamp to) {
+ public GetReflog setTo(Instant to) {
this.to = to;
return this;
}
private int limit;
- private Timestamp from;
- private Timestamp to;
+ private Instant from;
+ private Instant to;
@Inject
public GetReflog(GitRepositoryManager repoManager, PermissionBackend permissionBackend) {
@@ -89,6 +89,9 @@
this.permissionBackend = permissionBackend;
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Override
public Response<List<ReflogEntryInfo>> apply(BranchResource rsrc)
throws RestApiException, IOException, PermissionBackendException {
@@ -103,7 +106,7 @@
r = repo.getReflogReader(rsrc.getRef());
} catch (UnsupportedOperationException e) {
String msg = "reflog not supported on repo " + rsrc.getNameKey().get();
- logger.atSevere().log(msg);
+ logger.atSevere().log("%s", msg);
throw new MethodNotAllowedException(msg, e);
}
if (r == null) {
@@ -115,8 +118,8 @@
} else {
entries = limit > 0 ? new ArrayList<>(limit) : new ArrayList<>();
for (ReflogEntry e : r.getReverseEntries()) {
- Timestamp timestamp = new Timestamp(e.getWho().getWhen().getTime());
- if ((from == null || from.before(timestamp)) && (to == null || to.after(timestamp))) {
+ Instant timestamp = e.getWho().getWhen().toInstant();
+ if ((from == null || from.isBefore(timestamp)) && (to == null || to.isAfter(timestamp))) {
entries.add(e);
}
if (limit > 0 && entries.size() >= limit) {
diff --git a/java/com/google/gerrit/server/restapi/project/ListDashboards.java b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
index 4406719..9029e11 100644
--- a/java/com/google/gerrit/server/restapi/project/ListDashboards.java
+++ b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
@@ -100,7 +100,7 @@
return tree.values();
}
- private List<DashboardInfo> scan(ProjectState state, String project, boolean setDefault)
+ private ImmutableList<DashboardInfo> scan(ProjectState state, String project, boolean setDefault)
throws ResourceNotFoundException, IOException, PermissionBackendException {
if (!state.statePermitsRead()) {
return ImmutableList.of();
@@ -118,13 +118,13 @@
// Do nothing.
}
}
- return all;
+ return ImmutableList.copyOf(all);
} catch (RepositoryNotFoundException e) {
throw new ResourceNotFoundException(project, e);
}
}
- private List<DashboardInfo> scanDashboards(
+ private ImmutableList<DashboardInfo> scanDashboards(
Project definingProject,
Repository git,
RevWalk rw,
@@ -155,6 +155,6 @@
}
}
}
- return list;
+ return ImmutableList.copyOf(list);
}
}
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjects.java b/java/com/google/gerrit/server/restapi/project/ListProjects.java
index 4d8005b..5706016 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjects.java
@@ -76,9 +76,9 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.NavigableSet;
import java.util.Optional;
import java.util.SortedMap;
-import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Stream;
@@ -680,7 +680,7 @@
private void printProjectTree(
final PrintWriter stdout, TreeMap<Project.NameKey, ProjectNode> treeMap) {
- final SortedSet<ProjectNode> sortedNodes = new TreeSet<>();
+ final NavigableSet<ProjectNode> sortedNodes = new TreeSet<>();
// Builds the inheritance tree using a list.
//
diff --git a/java/com/google/gerrit/server/restapi/project/ListTags.java b/java/com/google/gerrit/server/restapi/project/ListTags.java
index 123c78a..eccdcfc 100644
--- a/java/com/google/gerrit/server/restapi/project/ListTags.java
+++ b/java/com/google/gerrit/server/restapi/project/ListTags.java
@@ -39,7 +39,7 @@
import com.google.gerrit.server.project.RefFilter;
import com.google.inject.Inject;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -172,6 +172,9 @@
throw new ResourceNotFoundException(id);
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
public static TagInfo createTagInfo(
PermissionBackend.ForRef perm, Ref ref, RevWalk rw, ProjectState projectState, WebLinks links)
throws IOException {
@@ -197,12 +200,12 @@
tagger != null ? CommonConverters.toGitPerson(tagger) : null,
canDelete,
webLinks.isEmpty() ? null : webLinks,
- tagger != null ? new Timestamp(tagger.getWhen().getTime()) : null);
+ tagger != null ? tagger.getWhen().toInstant() : null);
}
- Timestamp timestamp =
+ Instant timestamp =
object instanceof RevCommit
- ? new Timestamp(((RevCommit) object).getCommitterIdent().getWhen().getTime())
+ ? ((RevCommit) object).getCommitterIdent().getWhen().toInstant()
: null;
// Lightweight tag
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectNode.java b/java/com/google/gerrit/server/restapi/project/ProjectNode.java
index 1e6200c..816c69d 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectNode.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectNode.java
@@ -19,7 +19,7 @@
import com.google.gerrit.server.util.TreeFormatter.TreeNode;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import java.util.SortedSet;
+import java.util.NavigableSet;
import java.util.TreeSet;
/** Node of a Project in a tree formatted by {@link ListProjects}. */
@@ -32,7 +32,7 @@
private final Project project;
private final boolean isVisible;
- private final SortedSet<ProjectNode> children = new TreeSet<>();
+ private final NavigableSet<ProjectNode> children = new TreeSet<>();
@Inject
protected ProjectNode(
@@ -72,7 +72,7 @@
}
@Override
- public SortedSet<? extends ProjectNode> getChildren() {
+ public NavigableSet<? extends ProjectNode> getChildren() {
return children;
}
diff --git a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
index 65cc5a2..0a9503f 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.restapi.project;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.entities.AccessSection;
@@ -42,7 +43,6 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -66,10 +66,10 @@
this.pluginPermissionsUtil = pluginPermissionsUtil;
}
- List<AccessSection> getAccessSections(Map<String, AccessSectionInfo> sectionInfos)
+ ImmutableList<AccessSection> getAccessSections(Map<String, AccessSectionInfo> sectionInfos)
throws UnprocessableEntityException {
if (sectionInfos == null) {
- return Collections.emptyList();
+ return ImmutableList.of();
}
List<AccessSection> sections = new ArrayList<>(sectionInfos.size());
@@ -120,7 +120,7 @@
}
sections.add(accessSection.build());
}
- return sections;
+ return ImmutableList.copyOf(sections);
}
/**
diff --git a/java/com/google/gerrit/server/restapi/project/SetLabel.java b/java/com/google/gerrit/server/restapi/project/SetLabel.java
index 801fac7..79bb4ee 100644
--- a/java/com/google/gerrit/server/restapi/project/SetLabel.java
+++ b/java/com/google/gerrit/server/restapi/project/SetLabel.java
@@ -38,6 +38,7 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
+import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
@@ -147,6 +148,12 @@
}
}
+ if (input.description != null) {
+ String description = Strings.emptyToNull(input.description.trim());
+ labelTypeBuilder.setDescription(Optional.ofNullable(description));
+ dirty = true;
+ }
+
if (input.function != null) {
if (input.function.trim().isEmpty()) {
throw new BadRequestException("function cannot be empty");
diff --git a/java/com/google/gerrit/server/rules/PrologEnvironment.java b/java/com/google/gerrit/server/rules/PrologEnvironment.java
index bc0bb1a..2bf4175 100644
--- a/java/com/google/gerrit/server/rules/PrologEnvironment.java
+++ b/java/com/google/gerrit/server/rules/PrologEnvironment.java
@@ -35,10 +35,10 @@
import com.googlecode.prolog_cafe.lang.PredicateEncoder;
import com.googlecode.prolog_cafe.lang.Prolog;
import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
+import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.eclipse.jgit.lib.Config;
@@ -73,7 +73,7 @@
setEnabled(EnumSet.allOf(Prolog.Feature.class), false);
args = a;
storedValues = new HashMap<>();
- cleanup = new LinkedList<>();
+ cleanup = new ArrayList<>();
}
public Args getArgs() {
diff --git a/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java b/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
index 179a3d0..ddc3fca 100644
--- a/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
+++ b/java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
@@ -321,7 +321,7 @@
private SubmitRecord ruleError(String err, Exception e) {
if (opts.logErrors()) {
- logger.atSevere().withCause(e).log(err);
+ logger.atSevere().withCause(e).log("%s", err);
return createRuleError(DEFAULT_MSG);
}
logger.atFine().log("rule error: %s", err);
@@ -400,8 +400,7 @@
private SubmitTypeRecord typeError(String err, Exception e) {
if (opts.logErrors()) {
- logger.atSevere().withCause(e).log(err);
- return typeError(DEFAULT_MSG);
+ logger.atSevere().withCause(e).log("%s", err);
}
return SubmitTypeRecord.error(err);
}
diff --git a/java/com/google/gerrit/server/rules/StoredValues.java b/java/com/google/gerrit/server/rules/StoredValues.java
index fd66a3a..dbaefb9 100644
--- a/java/com/google/gerrit/server/rules/StoredValues.java
+++ b/java/com/google/gerrit/server/rules/StoredValues.java
@@ -29,6 +29,7 @@
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.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.project.ProjectState;
@@ -70,7 +71,7 @@
}
public static final StoredValue<RevCommit> COMMIT =
- new StoredValue<RevCommit>() {
+ new StoredValue<>() {
@Override
public RevCommit createValue(Prolog engine) {
Change change = getChange(engine);
@@ -86,7 +87,7 @@
};
public static final StoredValue<Map<String, FileDiffOutput>> DIFF_LIST =
- new StoredValue<Map<String, FileDiffOutput>>() {
+ new StoredValue<>() {
@Override
public Map<String, FileDiffOutput> createValue(Prolog engine) {
PrologEnvironment env = (PrologEnvironment) engine.control;
@@ -98,7 +99,7 @@
try {
diffList =
diffOperations.listModifiedFilesAgainstParent(
- project, ps.commitId(), /* parentNum= */ 0);
+ project, ps.commitId(), /* parentNum= */ 0, DiffOptions.DEFAULTS);
} catch (DiffNotAvailableException e) {
throw new SystemException(
String.format(
@@ -113,7 +114,7 @@
// It should be minimized or cached to reduce pause time
// when evaluating Prolog submit rules.
public static final StoredValue<GitRepositoryManager> REPO_MANAGER =
- new StoredValue<GitRepositoryManager>() {
+ new StoredValue<>() {
@Override
public GitRepositoryManager createValue(Prolog engine) {
PrologEnvironment env = (PrologEnvironment) engine.control;
@@ -122,7 +123,7 @@
};
public static final StoredValue<PluginConfigFactory> PLUGIN_CONFIG_FACTORY =
- new StoredValue<PluginConfigFactory>() {
+ new StoredValue<>() {
@Override
public PluginConfigFactory createValue(Prolog engine) {
PrologEnvironment env = (PrologEnvironment) engine.control;
@@ -131,7 +132,7 @@
};
public static final StoredValue<Repository> REPOSITORY =
- new StoredValue<Repository>() {
+ new StoredValue<>() {
@Override
public Repository createValue(Prolog engine) {
PrologEnvironment env = (PrologEnvironment) engine.control;
@@ -150,7 +151,7 @@
};
public static final StoredValue<PermissionBackend> PERMISSION_BACKEND =
- new StoredValue<PermissionBackend>() {
+ new StoredValue<>() {
@Override
protected PermissionBackend createValue(Prolog engine) {
PrologEnvironment env = (PrologEnvironment) engine.control;
@@ -159,7 +160,7 @@
};
public static final StoredValue<AnonymousUser> ANONYMOUS_USER =
- new StoredValue<AnonymousUser>() {
+ new StoredValue<>() {
@Override
protected AnonymousUser createValue(Prolog engine) {
PrologEnvironment env = (PrologEnvironment) engine.control;
@@ -168,7 +169,7 @@
};
public static final StoredValue<Map<Account.Id, IdentifiedUser>> USERS =
- new StoredValue<Map<Account.Id, IdentifiedUser>>() {
+ new StoredValue<>() {
@Override
protected Map<Account.Id, IdentifiedUser> createValue(Prolog engine) {
return new HashMap<>();
diff --git a/java/com/google/gerrit/server/schema/BUILD b/java/com/google/gerrit/server/schema/BUILD
index 0df7907..ce445e1 100644
--- a/java/com/google/gerrit/server/schema/BUILD
+++ b/java/com/google/gerrit/server/schema/BUILD
@@ -26,6 +26,5 @@
"//lib/commons:dbcp",
"//lib/flogger:api",
"//lib/guice",
- "//lib/log:log4j",
],
)
diff --git a/java/com/google/gerrit/server/securestore/SecureStoreProvider.java b/java/com/google/gerrit/server/securestore/SecureStoreProvider.java
index 4e43b2e..547b6dc 100644
--- a/java/com/google/gerrit/server/securestore/SecureStoreProvider.java
+++ b/java/com/google/gerrit/server/securestore/SecureStoreProvider.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.securestore;
import com.google.common.base.Strings;
-import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.SiteLibraryLoaderUtil;
import com.google.gerrit.server.config.SitePaths;
@@ -27,8 +26,6 @@
@Singleton
public class SecureStoreProvider implements Provider<SecureStore> {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
private final Path libdir;
private final Injector injector;
private final String className;
@@ -56,9 +53,7 @@
try {
return (Class<? extends SecureStore>) Class.forName(className);
} catch (ClassNotFoundException e) {
- String msg = String.format("Cannot load secure store class: %s", className);
- logger.atSevere().withCause(e).log(msg);
- throw new RuntimeException(msg, e);
+ throw new RuntimeException(String.format("Cannot load secure store class: %s", className), e);
}
}
}
diff --git a/java/com/google/gerrit/server/submit/CherryPick.java b/java/com/google/gerrit/server/submit/CherryPick.java
index a09ba63..84b0ab7 100644
--- a/java/com/google/gerrit/server/submit/CherryPick.java
+++ b/java/com/google/gerrit/server/submit/CherryPick.java
@@ -45,7 +45,7 @@
}
@Override
- public List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
+ public ImmutableList<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
List<CodeReviewCommit> sorted = CodeReviewCommit.ORDER.sortedCopy(toMerge);
List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
boolean first = true;
@@ -62,7 +62,7 @@
}
first = false;
}
- return ops;
+ return ImmutableList.copyOf(ops);
}
private class CherryPickRootOp extends SubmitStrategyOp {
@@ -102,8 +102,7 @@
args.rw.parseBody(mergeTip);
String cherryPickCmtMsg = args.mergeUtil.createCommitMessageOnSubmit(toMerge, mergeTip);
- PersonIdent committer =
- args.caller.newCommitterIdent(ctx.getWhen(), args.serverIdent.getTimeZone());
+ PersonIdent committer = ctx.newCommitterIdent(args.caller);
try {
newCommit =
args.mergeUtil.createCherryPickFromCommit(
@@ -196,7 +195,7 @@
&& !args.subscriptionGraph.hasSubscription(args.destBranch)) {
mergeTip.moveTipTo(toMerge, toMerge);
} else {
- PersonIdent myIdent = new PersonIdent(args.serverIdent, ctx.getWhen());
+ PersonIdent myIdent = ctx.newPersonIdent(args.serverIdent);
CodeReviewCommit result =
args.mergeUtil.mergeOneCommit(
myIdent,
diff --git a/java/com/google/gerrit/server/submit/FastForwardOnly.java b/java/com/google/gerrit/server/submit/FastForwardOnly.java
index 8a30898..ad01d31 100644
--- a/java/com/google/gerrit/server/submit/FastForwardOnly.java
+++ b/java/com/google/gerrit/server/submit/FastForwardOnly.java
@@ -30,7 +30,7 @@
}
@Override
- public List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
+ public ImmutableList<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
Map<BranchNameKey, CodeReviewCommit> branchToCommit = new HashMap<>();
@@ -57,7 +57,7 @@
ops.add(new NotFastForwardOp(c));
}
}
- return ops;
+ return ImmutableList.copyOf(ops);
}
private class NotFastForwardOp extends SubmitStrategyOp {
diff --git a/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java b/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
index 89ba1fa..2a260e41 100644
--- a/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
+++ b/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
@@ -253,7 +253,7 @@
}
private void logErrorAndThrow(String msg) {
- logger.atSevere().log(msg);
+ logger.atSevere().log("%s", msg);
throw new StorageException(msg);
}
}
diff --git a/java/com/google/gerrit/server/submit/MergeAlways.java b/java/com/google/gerrit/server/submit/MergeAlways.java
index c3f186a..7258448 100644
--- a/java/com/google/gerrit/server/submit/MergeAlways.java
+++ b/java/com/google/gerrit/server/submit/MergeAlways.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.submit;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.server.git.CodeReviewCommit;
import java.util.ArrayList;
import java.util.Collection;
@@ -25,7 +26,7 @@
}
@Override
- public List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
+ public ImmutableList<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
if (args.mergeTip.getInitialTip() == null && !sorted.isEmpty()) {
@@ -38,7 +39,7 @@
CodeReviewCommit n = sorted.remove(0);
ops.add(new MergeOneOp(args, n));
}
- return ops;
+ return ImmutableList.copyOf(ops);
}
static boolean dryRun(
diff --git a/java/com/google/gerrit/server/submit/MergeIfNecessary.java b/java/com/google/gerrit/server/submit/MergeIfNecessary.java
index 30f1661..29fc240 100644
--- a/java/com/google/gerrit/server/submit/MergeIfNecessary.java
+++ b/java/com/google/gerrit/server/submit/MergeIfNecessary.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.submit;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.server.git.CodeReviewCommit;
import java.util.ArrayList;
import java.util.Collection;
@@ -25,7 +26,7 @@
}
@Override
- public List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
+ public ImmutableList<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
@@ -43,7 +44,7 @@
CodeReviewCommit n = sorted.remove(0);
ops.add(new MergeOneOp(args, n));
}
- return ops;
+ return ImmutableList.copyOf(ops);
}
static boolean dryRun(
diff --git a/java/com/google/gerrit/server/submit/MergeOneOp.java b/java/com/google/gerrit/server/submit/MergeOneOp.java
index f1b93e1..1840479 100644
--- a/java/com/google/gerrit/server/submit/MergeOneOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOneOp.java
@@ -29,9 +29,7 @@
@Override
public void updateRepoImpl(RepoContext ctx) throws IntegrationConflictException, IOException {
- PersonIdent caller =
- ctx.getIdentifiedUser()
- .newCommitterIdent(args.serverIdent.getWhen(), args.serverIdent.getTimeZone());
+ PersonIdent caller = ctx.getIdentifiedUser().newCommitterIdent(args.serverIdent);
if (args.mergeTip.getCurrentTip() == null) {
throw new IllegalStateException(
"cannot merge commit "
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 942f024..0160fc9 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -36,13 +36,14 @@
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Change.Status;
-import com.google.gerrit.entities.LegacySubmitRequirement;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.SubmissionId;
import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.entities.SubmitTypeRecord;
-import com.google.gerrit.exceptions.InternalServerWithUserMessageException;
+import com.google.gerrit.exceptions.MergeUpdateException;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.SubmitInput;
@@ -88,7 +89,7 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -96,6 +97,7 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -246,7 +248,7 @@
// Changes that were updated by this MergeOp.
private final Map<Change.Id, Change> updatedChanges;
- private Timestamp ts;
+ private Instant ts;
private SubmissionId submissionId;
private IdentifiedUser caller;
@@ -303,43 +305,46 @@
}
}
- public static void checkSubmitRule(ChangeData cd, boolean allowClosed)
- throws ResourceConflictException {
+ public static void checkSubmitRequirements(ChangeData cd) throws ResourceConflictException {
PatchSet patchSet = cd.currentPatchSet();
if (patchSet == null) {
throw new ResourceConflictException("missing current patch set for change " + cd.getId());
}
- List<SubmitRecord> results = getSubmitRecords(cd, allowClosed);
- if (SubmitRecord.allRecordsOK(results)) {
- // Rules supplied a valid solution.
+ Map<SubmitRequirement, SubmitRequirementResult> srResults = cd.submitRequirements();
+ if (srResults.values().stream().allMatch(SubmitRequirementResult::fulfilled)) {
return;
- } else if (results.isEmpty()) {
+ } else if (srResults.isEmpty()) {
throw new IllegalStateException(
String.format(
- "SubmitRuleEvaluator.evaluate for change %s returned empty list for %s in %s",
+ "Submit requirement results for change '%s' and patchset '%s' "
+ + "are empty in project '%s'",
cd.getId(), patchSet.id(), cd.change().getProject().get()));
}
- for (SubmitRecord record : results) {
- switch (record.status) {
- case OK:
+ for (SubmitRequirementResult srResult : srResults.values()) {
+ switch (srResult.status()) {
+ case SATISFIED:
+ case NOT_APPLICABLE:
+ case OVERRIDDEN:
+ case FORCED:
break;
- case CLOSED:
- throw new ResourceConflictException("change is closed");
+ case ERROR:
+ throw new ResourceConflictException(
+ String.format(
+ "submit requirement '%s' has an error: %s",
+ srResult.submitRequirement().name(), srResult.errorMessage().orElse("")));
- case RULE_ERROR:
- throw new ResourceConflictException("submit rule error: " + record.errorMessage);
+ case UNSATISFIED:
+ throw new ResourceConflictException(
+ String.format(
+ "submit requirement '%s' is unsatisfied.", srResult.submitRequirement().name()));
- case NOT_READY:
- throw new ResourceConflictException(describeNotReady(cd, record));
-
- case FORCED:
default:
throw new IllegalStateException(
String.format(
- "Unexpected SubmitRecord status %s for %s in %s",
- record.status, patchSet.id().getId(), cd.change().getProject().get()));
+ "Unexpected submit requirement status %s for %s in %s",
+ srResult.status().name(), patchSet.id().getId(), cd.change().getProject().get()));
}
}
throw new IllegalStateException();
@@ -349,56 +354,8 @@
return allowClosed ? SUBMIT_RULE_OPTIONS_ALLOW_CLOSED : SUBMIT_RULE_OPTIONS;
}
- private static List<SubmitRecord> getSubmitRecords(ChangeData cd, boolean allowClosed) {
- return cd.submitRecords(submitRuleOptions(allowClosed));
- }
-
- private static String describeNotReady(ChangeData cd, SubmitRecord record) {
- List<String> blockingConditions = new ArrayList<>();
- if (record.labels != null) {
- blockingConditions.add(describeLabels(cd, record.labels));
- }
- if (record.requirements != null) {
- record.requirements.stream()
- .map(MergeOp::describeSubmitRequirement)
- .forEach(blockingConditions::add);
- }
- return Joiner.on("; ").join(blockingConditions);
- }
-
- private static String describeLabels(ChangeData cd, List<SubmitRecord.Label> labels) {
- List<String> labelResults = new ArrayList<>();
- for (SubmitRecord.Label lbl : labels) {
- switch (lbl.status) {
- case OK:
- case MAY:
- break;
-
- case REJECT:
- labelResults.add("blocked by " + lbl.label);
- break;
-
- case NEED:
- labelResults.add("needs " + lbl.label);
- break;
-
- case IMPOSSIBLE:
- labelResults.add("needs " + lbl.label + " (check project access)");
- break;
-
- default:
- throw new IllegalStateException(
- String.format(
- "Unsupported SubmitRecord.Label %s for %s in %s",
- lbl, cd.change().currentPatchSetId(), cd.change().getProject()));
- }
- }
- return Joiner.on("; ").join(labelResults);
- }
-
- private static String describeSubmitRequirement(LegacySubmitRequirement legacySubmitRequirement) {
- return String.format(
- "Submit requirement not fulfilled: %s", legacySubmitRequirement.fallbackText());
+ private static List<SubmitRecord> getSubmitRecords(ChangeData cd) {
+ return cd.submitRecords(submitRuleOptions(/* allowClosed= */ false));
}
private void checkSubmitRulesAndState(ChangeSet cs, boolean allowMerged)
@@ -415,7 +372,7 @@
} else if (cd.change().isWorkInProgress()) {
commitStatus.problem(cd.getId(), "Change " + cd.getId() + " is work in progress");
} else {
- checkSubmitRule(cd, allowMerged);
+ checkSubmitRequirements(cd);
}
} catch (ResourceConflictException e) {
commitStatus.problem(cd.getId(), e.getMessage());
@@ -428,15 +385,32 @@
commitStatus.maybeFailVerbose();
}
- private void bypassSubmitRules(ChangeSet cs, boolean allowClosed) {
+ private void bypassSubmitRulesAndRequirements(ChangeSet cs) {
checkArgument(
!cs.furtherHiddenChanges(), "cannot bypass submit rules for topic with hidden change");
for (ChangeData cd : cs.changes()) {
- List<SubmitRecord> records = new ArrayList<>(getSubmitRecords(cd, allowClosed));
+ Change change = cd.change();
+ if (change == null) {
+ throw new StorageException("Change not found");
+ }
+ if (change.isClosed()) {
+ // No need to check submit rules if the change is closed.
+ continue;
+ }
+ List<SubmitRecord> records = new ArrayList<>(getSubmitRecords(cd));
SubmitRecord forced = new SubmitRecord();
forced.status = SubmitRecord.Status.FORCED;
records.add(forced);
- cd.setSubmitRecords(submitRuleOptions(allowClosed), records);
+ cd.setSubmitRecords(submitRuleOptions(/* allowClosed= */ false), records);
+
+ // Also bypass submit requirements. Mark them as forced.
+ Map<SubmitRequirement, SubmitRequirementResult> forcedSRs =
+ cd.submitRequirements().entrySet().stream()
+ .collect(
+ Collectors.toMap(
+ Map.Entry::getKey,
+ entry -> entry.getValue().toBuilder().forced(Optional.of(true)).build()));
+ cd.setSubmitRequirements(forcedSRs);
}
}
@@ -470,7 +444,7 @@
firstNonNull(submitInput.notify, NotifyHandling.ALL), submitInput.notifyDetails);
this.dryrun = dryrun;
this.caller = caller;
- this.ts = TimeUtil.nowTs();
+ this.ts = TimeUtil.now();
this.submissionId = new SubmissionId(change);
try (TraceContext traceContext =
@@ -481,7 +455,9 @@
logger.atFine().log("Beginning integration of %s", change);
try {
ChangeSet indexBackedChangeSet =
- mergeSuperSet.setMergeOpRepoManager(orm).completeChangeSet(change, caller);
+ mergeSuperSet
+ .setMergeOpRepoManager(orm)
+ .completeChangeSet(change, caller, /* includingTopicClosure= */ false);
if (!indexBackedChangeSet.ids().contains(change.getId())) {
// indexBackedChangeSet contains only open changes, if the change is missing in this set
// it might be that the change was concurrently submitted in the meantime.
@@ -509,7 +485,7 @@
if (!changeData.change().getStatus().equals(Status.NEW)) {
logger.atFine().log(
"Change %s has status %s due to stale index, so it is skipped during submit",
- changeData.getId().toString(), changeData.change().getStatus().name());
+ changeData.getId(), changeData.change().getStatus().name());
continue;
}
filteredChanges.add(changeData);
@@ -538,7 +514,7 @@
boolean isRetry = attempt > 1;
if (isRetry) {
logger.atFine().log("Retrying, attempt #%d; skipping merged changes", attempt);
- this.ts = TimeUtil.nowTs();
+ this.ts = TimeUtil.now();
openRepoManager();
}
this.commitStatus = new CommitStatus(filteredNoteDbChangeSet, isRetry);
@@ -547,7 +523,7 @@
checkSubmitRulesAndState(filteredNoteDbChangeSet, isRetry);
} else {
logger.atFine().log("Bypassing submit rules");
- bypassSubmitRules(filteredNoteDbChangeSet, isRetry);
+ bypassSubmitRulesAndRequirements(filteredNoteDbChangeSet);
}
integrateIntoHistory(filteredNoteDbChangeSet, submissionExecutor);
return null;
@@ -666,9 +642,12 @@
for (Map.Entry<Change.Id, ChangeData> entry : cs.changesById().entrySet()) {
Project.NameKey project = entry.getValue().project();
Change.Id changeId = entry.getKey();
+ ChangeData cd = entry.getValue();
batchUpdatesByProject
.get(project)
- .addOp(changeId, storeSubmitRequirementsOpFactory.create());
+ .addOp(
+ changeId,
+ storeSubmitRequirementsOpFactory.create(cd.submitRequirements().values()));
}
try {
submissionExecutor.setAdditionalBatchUpdateListeners(
@@ -711,7 +690,7 @@
if (e.getCause() instanceof IntegrationConflictException) {
throw (IntegrationConflictException) e.getCause();
}
- throw new InternalServerWithUserMessageException(genericMergeError(cs), e);
+ throw new MergeUpdateException(genericMergeError(cs), e);
}
}
diff --git a/java/com/google/gerrit/server/submit/MergeOpRepoManager.java b/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
index 8981b07..2024448 100644
--- a/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
+++ b/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
@@ -37,7 +37,7 @@
import com.google.gerrit.server.update.BatchUpdate;
import com.google.inject.Inject;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -167,7 +167,7 @@
private final GitRepositoryManager repoManager;
private final ProjectCache projectCache;
- private Timestamp ts;
+ private Instant ts;
private IdentifiedUser caller;
private NotifyResolver.Result notify;
@@ -185,7 +185,7 @@
openRepos = new HashMap<>();
}
- public void setContext(Timestamp ts, IdentifiedUser caller, NotifyResolver.Result notify) {
+ public void setContext(Instant ts, IdentifiedUser caller, NotifyResolver.Result notify) {
this.ts = requireNonNull(ts);
this.caller = requireNonNull(caller);
this.notify = requireNonNull(notify);
diff --git a/java/com/google/gerrit/server/submit/MergeSuperSet.java b/java/com/google/gerrit/server/submit/MergeSuperSet.java
index 67f2907..8581e20 100644
--- a/java/com/google/gerrit/server/submit/MergeSuperSet.java
+++ b/java/com/google/gerrit/server/submit/MergeSuperSet.java
@@ -92,7 +92,19 @@
return this;
}
- public ChangeSet completeChangeSet(Change change, CurrentUser user)
+ /**
+ * Gets the ChangeSet of this {@code change} based on visiblity of the {@code user}. if
+ * change.submitWholeTopic is true, we return the topic closure as well as the dependent changes
+ * of the topic closure. Otherwise, we return just the dependent changes.
+ *
+ * @param change the change for which we get the dependent changes / topic closure.
+ * @param user the current user for visibility purposes.
+ * @param includingTopicClosure when true, return as if change.submitWholeTopic = true, so we
+ * return the topic closure.
+ * @return {@link ChangeSet} object that represents the dependent changes and/or topic closure of
+ * the requested change.
+ */
+ public ChangeSet completeChangeSet(Change change, CurrentUser user, boolean includingTopicClosure)
throws IOException, PermissionBackendException {
try {
if (orm == null) {
@@ -113,7 +125,7 @@
}
ChangeSet changeSet = new ChangeSet(cd, visible);
- if (wholeTopicEnabled(cfg)) {
+ if (wholeTopicEnabled(cfg) || includingTopicClosure) {
return completeChangeSetIncludingTopics(changeSet, user);
}
try (TraceContext traceContext = PluginContext.newTrace(mergeSuperSetComputation)) {
diff --git a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
index 355d25f..1409775 100644
--- a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
@@ -15,9 +15,9 @@
package com.google.gerrit.server.submit;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.server.submit.CommitMergeStatus.EMPTY_COMMIT;
import static com.google.gerrit.server.submit.CommitMergeStatus.SKIPPED_IDENTICAL_TREE;
-import static java.util.stream.Collectors.toList;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.BooleanProjectConfig;
@@ -56,7 +56,7 @@
}
@Override
- public List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
+ public ImmutableList<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) {
List<CodeReviewCommit> sorted;
try {
sorted = args.rebaseSorter.sort(toMerge);
@@ -92,7 +92,7 @@
// found a merge commit that depends on a normal change, this means we are required to merge
// the whole series at once
sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, sorted);
- return sorted.stream().map(n -> new MergeIfNecessaryOp(n)).collect(toList());
+ return sorted.stream().map(n -> new MergeIfNecessaryOp(n)).collect(toImmutableList());
}
foundNonMerge = true;
}
@@ -114,7 +114,7 @@
}
first = false;
}
- return ops;
+ return ImmutableList.copyOf(ops);
}
private class RebaseRootOp extends SubmitStrategyOp {
@@ -166,8 +166,7 @@
RevCommit mergeTip = args.mergeTip.getCurrentTip();
args.rw.parseBody(mergeTip);
String cherryPickCmtMsg = args.mergeUtil.createCommitMessageOnSubmit(toMerge, mergeTip);
- PersonIdent committer =
- args.caller.newCommitterIdent(ctx.getWhen(), args.serverIdent.getTimeZone());
+ PersonIdent committer = ctx.newCommitterIdent(args.caller);
try {
newCommit =
args.mergeUtil.createCherryPickFromCommit(
@@ -304,8 +303,7 @@
&& !args.subscriptionGraph.hasSubscription(args.destBranch)) {
mergeTip.moveTipTo(toMerge, toMerge);
} else {
- PersonIdent caller =
- ctx.getIdentifiedUser().newCommitterIdent(ctx.getWhen(), ctx.getTimeZone());
+ PersonIdent caller = ctx.newCommitterIdent();
CodeReviewCommit newTip =
args.mergeUtil.mergeOneCommit(
caller,
diff --git a/java/com/google/gerrit/server/submit/SubmitDryRun.java b/java/com/google/gerrit/server/submit/SubmitDryRun.java
index bcd7923..cee0ad9 100644
--- a/java/com/google/gerrit/server/submit/SubmitDryRun.java
+++ b/java/com/google/gerrit/server/submit/SubmitDryRun.java
@@ -152,7 +152,7 @@
case INHERIT:
default:
String errorMsg = "No submit strategy for: " + submitType;
- logger.atSevere().log(errorMsg);
+ logger.atSevere().log("%s", errorMsg);
throw new StorageException(errorMsg);
}
}
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategy.java b/java/com/google/gerrit/server/submit/SubmitStrategy.java
index 6291e6c..83c6634 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategy.java
@@ -18,6 +18,7 @@
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static java.util.Objects.requireNonNull;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.gerrit.entities.BranchNameKey;
@@ -293,5 +294,5 @@
}
}
- protected abstract List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge);
+ protected abstract ImmutableList<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge);
}
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java b/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java
index 2e66ae2..3bd26dc 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java
@@ -90,7 +90,7 @@
case INHERIT:
default:
String errorMsg = "No submit strategy for: " + submitType;
- logger.atSevere().log(errorMsg);
+ logger.atSevere().log("%s", errorMsg);
throw new StorageException(errorMsg);
}
}
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index f26ec17..d06940c 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -286,7 +286,7 @@
setMerged(ctx, commit, message(ctx, commit, s));
} catch (StorageException err) {
String msg = "Error updating change status for " + id;
- logger.atSevere().withCause(err).log(msg);
+ logger.atSevere().withCause(err).log("%s", msg);
args.commitStatus.logProblem(id, msg);
// It's possible this happened before updating anything in the db, but
// it's hard to know for sure, so just return true below to be safe.
@@ -327,7 +327,7 @@
ctx.getRevWalk(), ctx.getUpdate(psId), psId, alreadyMergedCommit, groups, null, null);
}
- private void setApproval(ChangeContext ctx, IdentifiedUser user) throws IOException {
+ private void setApproval(ChangeContext ctx, IdentifiedUser user) {
Change.Id id = ctx.getChange().getId();
List<SubmitRecord> records = args.commitStatus.getSubmitRecords(id);
PatchSet.Id oldPsId = toMerge.getPatchsetId();
@@ -348,13 +348,10 @@
}
}
- private LabelNormalizer.Result approve(ChangeContext ctx, ChangeUpdate update)
- throws IOException {
+ private LabelNormalizer.Result approve(ChangeContext ctx, ChangeUpdate update) {
PatchSet.Id psId = update.getPatchSetId();
Map<PatchSetApproval.Key, PatchSetApproval> byKey = new HashMap<>();
- for (PatchSetApproval psa :
- args.approvalsUtil.byPatchSet(
- ctx.getNotes(), psId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) {
+ for (PatchSetApproval psa : args.approvalsUtil.byPatchSet(ctx.getNotes(), psId)) {
byKey.put(psa.key(), psa);
}
@@ -521,19 +518,29 @@
}
}
- /** See {@link #updateRepo(RepoContext)} */
+ /**
+ * See {@link #updateRepo(RepoContext)}
+ *
+ * @param ctx context for the repository update
+ */
protected void updateRepoImpl(RepoContext ctx) throws Exception {}
/**
* Returns a new patch set if one was created by the submit strategy, or null if not
*
* <p>See {@link #updateChange(ChangeContext)}
+ *
+ * @param ctx context for the change update
*/
protected PatchSet updateChangeImpl(ChangeContext ctx) throws Exception {
return null;
}
- /** See {@link #postUpdate(PostUpdateContext)} */
+ /**
+ * See {@link #postUpdate(PostUpdateContext)}
+ *
+ * @param ctx context for the post update
+ */
protected void postUpdateImpl(PostUpdateContext ctx) throws Exception {}
/** Amend the commit with gitlink update */
diff --git a/java/com/google/gerrit/server/tools/ToolsCatalog.java b/java/com/google/gerrit/server/tools/ToolsCatalog.java
index 9c1483f..015b8f1 100644
--- a/java/com/google/gerrit/server/tools/ToolsCatalog.java
+++ b/java/com/google/gerrit/server/tools/ToolsCatalog.java
@@ -31,7 +31,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-import java.util.SortedMap;
+import java.util.NavigableMap;
import java.util.TreeMap;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.util.RawParseUtils;
@@ -46,7 +46,7 @@
public class ToolsCatalog {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- private final SortedMap<String, Entry> toc;
+ private final NavigableMap<String, Entry> toc;
@Inject
ToolsCatalog() throws IOException {
@@ -73,8 +73,8 @@
return toc.get(name);
}
- private static SortedMap<String, Entry> readToc() throws IOException {
- SortedMap<String, Entry> toc = new TreeMap<>();
+ private static NavigableMap<String, Entry> readToc() throws IOException {
+ NavigableMap<String, Entry> toc = new TreeMap<>();
final BufferedReader br =
new BufferedReader(new InputStreamReader(new ByteArrayInputStream(read("TOC")), UTF_8));
String line;
@@ -108,7 +108,7 @@
}
toc.put(top.getPath(), top);
- return Collections.unmodifiableSortedMap(toc);
+ return Collections.unmodifiableNavigableMap(toc);
}
@Nullable
diff --git a/java/com/google/gerrit/server/update/BatchUpdate.java b/java/com/google/gerrit/server/update/BatchUpdate.java
index 917e967..9edfdc4 100644
--- a/java/com/google/gerrit/server/update/BatchUpdate.java
+++ b/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -68,7 +68,7 @@
import com.google.inject.Module;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -122,7 +122,7 @@
}
public interface Factory {
- BatchUpdate create(Project.NameKey project, CurrentUser user, Timestamp when);
+ BatchUpdate create(Project.NameKey project, CurrentUser user, Instant when);
}
public static void execute(
@@ -252,7 +252,7 @@
}
@Override
- public Timestamp getWhen() {
+ public Instant getWhen() {
return when;
}
@@ -376,7 +376,7 @@
private final Project.NameKey project;
private final CurrentUser user;
- private final Timestamp when;
+ private final Instant when;
private final TimeZone tz;
private final ListMultimap<Change.Id, BatchUpdateOp> ops =
@@ -405,7 +405,7 @@
GitReferenceUpdated gitRefUpdated,
@Assisted Project.NameKey project,
@Assisted CurrentUser user,
- @Assisted Timestamp when) {
+ @Assisted Instant when) {
this.repoManager = repoManager;
this.changeDataFactory = changeDataFactory;
this.changeNotesFactory = changeNotesFactory;
@@ -615,7 +615,7 @@
BatchUpdate.this.executed = manager.isExecuted();
}
- List<ListenableFuture<ChangeData>> startIndexFutures() {
+ ImmutableList<ListenableFuture<ChangeData>> startIndexFutures() {
if (dryrun) {
return ImmutableList.of();
}
@@ -636,7 +636,7 @@
throw new IllegalStateException("unexpected result: " + e.getValue());
}
}
- return indexFutures;
+ return ImmutableList.copyOf(indexFutures);
}
}
@@ -736,7 +736,7 @@
// expensive/complicated requests like MergeOp. Doing it every time would be
// noisy.
if (RequestId.isSet()) {
- logger.atFine().log(msg);
+ logger.atFine().log("%s", msg);
}
}
diff --git a/java/com/google/gerrit/server/update/Context.java b/java/com/google/gerrit/server/update/Context.java
index 9947168..57ebedd 100644
--- a/java/com/google/gerrit/server/update/Context.java
+++ b/java/com/google/gerrit/server/update/Context.java
@@ -24,8 +24,9 @@
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.change.NotifyResolver;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.TimeZone;
+import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevWalk;
/**
@@ -66,7 +67,7 @@
*
* @return timestamp.
*/
- Timestamp getWhen();
+ Instant getWhen();
/**
* Get the time zone in which this update takes place.
@@ -134,4 +135,33 @@
default Account.Id getAccountId() {
return getIdentifiedUser().getAccountId();
}
+
+ /**
+ * Creates a new {@link PersonIdent} with {@link #getWhen()} as timestamp.
+ *
+ * @param personIdent {@link PersonIdent} to be copied
+ * @return copied {@link PersonIdent} with {@link #getWhen()} as timestamp
+ */
+ default PersonIdent newPersonIdent(PersonIdent personIdent) {
+ return new PersonIdent(personIdent, getWhen().toEpochMilli(), personIdent.getTimeZoneOffset());
+ }
+
+ /**
+ * Creates a committer {@link PersonIdent} for {@link #getIdentifiedUser()}.
+ *
+ * @return the created committer {@link PersonIdent}
+ */
+ default PersonIdent newCommitterIdent() {
+ return newCommitterIdent(getIdentifiedUser());
+ }
+
+ /**
+ * Creates a committer {@link PersonIdent} for the given user.
+ *
+ * @param user user for which a committer {@link PersonIdent} should be created
+ * @return the created committer {@link PersonIdent}
+ */
+ default PersonIdent newCommitterIdent(IdentifiedUser user) {
+ return user.newCommitterIdent(getWhen(), getTimeZone());
+ }
}
diff --git a/java/com/google/gerrit/server/util/AccountTemplateUtil.java b/java/com/google/gerrit/server/util/AccountTemplateUtil.java
index c552ce8..1b39bef 100644
--- a/java/com/google/gerrit/server/util/AccountTemplateUtil.java
+++ b/java/com/google/gerrit/server/util/AccountTemplateUtil.java
@@ -82,7 +82,7 @@
/** Builds user-readable text from text, that might contain {@link #ACCOUNT_TEMPLATE}. */
public String replaceTemplates(String messageTemplate) {
Matcher matcher = ACCOUNT_TEMPLATE_PATTERN.matcher(messageTemplate);
- StringBuffer out = new StringBuffer();
+ StringBuilder out = new StringBuilder();
while (matcher.find()) {
String accountId = matcher.group(1);
String unrecognizedAccount = "Unrecognized Gerrit Account " + accountId;
diff --git a/java/com/google/gerrit/server/util/AttentionSetUtil.java b/java/com/google/gerrit/server/util/AttentionSetUtil.java
index 9238b44..26c8f47 100644
--- a/java/com/google/gerrit/server/util/AttentionSetUtil.java
+++ b/java/com/google/gerrit/server/util/AttentionSetUtil.java
@@ -28,7 +28,6 @@
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.notedb.ChangeNotes;
import java.io.IOException;
-import java.sql.Timestamp;
import java.util.Collection;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -43,6 +42,14 @@
.collect(ImmutableSet.toImmutableSet());
}
+ /** Returns only updates where the user was removed. */
+ public static ImmutableSet<AttentionSetUpdate> removalsOnly(
+ Collection<AttentionSetUpdate> updates) {
+ return updates.stream()
+ .filter(u -> u.operation() == Operation.REMOVE)
+ .collect(ImmutableSet.toImmutableSet());
+ }
+
/**
* Validates the input for AttentionSetInput. This must be called for all inputs that relate to
* adding or removing attention set entries, except for {@link
@@ -115,7 +122,7 @@
: null;
return new AttentionSetInfo(
accountLoader.get(attentionSetUpdate.account()),
- Timestamp.from(attentionSetUpdate.timestamp()),
+ attentionSetUpdate.timestamp(),
attentionSetUpdate.reason(),
reasonAccount);
}
diff --git a/java/com/google/gerrit/server/util/IdGenerator.java b/java/com/google/gerrit/server/util/IdGenerator.java
index d4c2dc4..1534ef3 100644
--- a/java/com/google/gerrit/server/util/IdGenerator.java
+++ b/java/com/google/gerrit/server/util/IdGenerator.java
@@ -62,7 +62,7 @@
private static short hi16(int in) {
return (short)
( //
- ((in >>> 24 & 0xff))
+ (in >>> 24 & 0xff)
| //
((in >>> 16 & 0xff) << 8) //
);
@@ -71,7 +71,7 @@
private static short lo16(int in) {
return (short)
( //
- ((in >>> 8 & 0xff))
+ (in >>> 8 & 0xff)
| //
((in & 0xff) << 8) //
);
diff --git a/java/com/google/gerrit/server/util/RequestScopePropagator.java b/java/com/google/gerrit/server/util/RequestScopePropagator.java
index 10c46fc..2f03b07 100644
--- a/java/com/google/gerrit/server/util/RequestScopePropagator.java
+++ b/java/com/google/gerrit/server/util/RequestScopePropagator.java
@@ -76,7 +76,7 @@
public final <T> Callable<T> wrap(Callable<T> callable) {
final RequestContext callerContext = requireNonNull(local.getContext());
final Callable<T> wrapped = wrapImpl(context(callerContext, cleanup(callable)));
- return new Callable<T>() {
+ return new Callable<>() {
@Override
public T call() throws Exception {
if (callerContext == local.getContext()) {
@@ -169,7 +169,7 @@
protected <T> Callable<T> context(RequestContext context, Callable<T> callable) {
return () -> {
- RequestContext old = local.setContext(context::getUser);
+ RequestContext old = local.setContext(context);
try {
return callable.call();
} finally {
diff --git a/java/com/google/gerrit/server/util/TreeFormatter.java b/java/com/google/gerrit/server/util/TreeFormatter.java
index 49d4a55..5a898d5 100644
--- a/java/com/google/gerrit/server/util/TreeFormatter.java
+++ b/java/com/google/gerrit/server/util/TreeFormatter.java
@@ -15,7 +15,7 @@
package com.google.gerrit.server.util;
import java.io.PrintWriter;
-import java.util.SortedSet;
+import java.util.NavigableSet;
public class TreeFormatter {
@@ -24,7 +24,7 @@
boolean isVisible();
- SortedSet<? extends TreeNode> getChildren();
+ NavigableSet<? extends TreeNode> getChildren();
}
public static final String NOT_VISIBLE_NODE = "(x)";
@@ -40,7 +40,7 @@
this.stdout = stdout;
}
- public void printTree(SortedSet<? extends TreeNode> rootNodes) {
+ public void printTree(NavigableSet<? extends TreeNode> rootNodes) {
if (rootNodes.isEmpty()) {
return;
}
@@ -66,7 +66,7 @@
private void printTree(TreeNode node, int level, boolean isLast) {
printNode(node, level, isLast);
- final SortedSet<? extends TreeNode> childNodes = node.getChildren();
+ final NavigableSet<? extends TreeNode> childNodes = node.getChildren();
int i = 0;
final int size = childNodes.size();
for (TreeNode childNode : childNodes) {
diff --git a/java/com/google/gerrit/server/util/time/TimeUtil.java b/java/com/google/gerrit/server/util/time/TimeUtil.java
index 54ef305..f89324b 100644
--- a/java/com/google/gerrit/server/util/time/TimeUtil.java
+++ b/java/com/google/gerrit/server/util/time/TimeUtil.java
@@ -15,10 +15,7 @@
package com.google.gerrit.server.util.time;
import com.google.common.annotations.VisibleForTesting;
-import com.google.gerrit.common.UsedAt;
-import com.google.gerrit.common.UsedAt.Project;
import com.google.gerrit.server.util.git.DelegateSystemReader;
-import java.sql.Timestamp;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import java.util.function.LongSupplier;
@@ -44,23 +41,8 @@
return Instant.ofEpochMilli(nowMs());
}
- public static Timestamp nowTs() {
- return new Timestamp(nowMs());
- }
-
- /**
- * Returns the magic timestamp representing no specific time.
- *
- * <p>This "null object" is helpful in contexts where using {@code null} directly is not possible.
- */
- @UsedAt(Project.PLUGIN_CHECKS)
- public static Timestamp never() {
- // Always create a new object as timestamps are mutable.
- return new Timestamp(0);
- }
-
- public static Timestamp truncateToSecond(Timestamp t) {
- return new Timestamp((t.getTime() / 1000) * 1000);
+ public static Instant truncateToSecond(Instant t) {
+ return Instant.ofEpochMilli(t.getEpochSecond() * 1000);
}
@VisibleForTesting
diff --git a/java/com/google/gerrit/sshd/AliasCommand.java b/java/com/google/gerrit/sshd/AliasCommand.java
index bf0dd91..17ea463 100644
--- a/java/com/google/gerrit/sshd/AliasCommand.java
+++ b/java/com/google/gerrit/sshd/AliasCommand.java
@@ -22,7 +22,7 @@
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import java.io.IOException;
-import java.util.LinkedList;
+import java.util.ArrayDeque;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
@@ -120,8 +120,8 @@
}
}
- private static LinkedList<String> chain(CommandName command) {
- LinkedList<String> chain = new LinkedList<>();
+ private static Iterable<String> chain(CommandName command) {
+ ArrayDeque<String> chain = new ArrayDeque<>();
while (command != null) {
chain.addFirst(command.value());
command = Commands.parentOf(command);
diff --git a/java/com/google/gerrit/sshd/BUILD b/java/com/google/gerrit/sshd/BUILD
index f3bd5e1..af7078d 100644
--- a/java/com/google/gerrit/sshd/BUILD
+++ b/java/com/google/gerrit/sshd/BUILD
@@ -4,7 +4,6 @@
name = "sshd",
srcs = glob(["**/*.java"]),
visibility = ["//visibility:public"],
- runtime_deps = ["//lib:jsch"],
deps = [
"//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/common:server",
diff --git a/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index 38ac26d..92de012 100644
--- a/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -39,11 +39,11 @@
import java.util.concurrent.atomic.AtomicReference;
import org.apache.sshd.server.Environment;
import org.apache.sshd.server.ExitCallback;
-import org.apache.sshd.server.SessionAware;
import org.apache.sshd.server.channel.ChannelSession;
import org.apache.sshd.server.command.Command;
import org.apache.sshd.server.command.CommandFactory;
import org.apache.sshd.server.session.ServerSession;
+import org.apache.sshd.server.session.ServerSessionAware;
import org.eclipse.jgit.lib.Config;
/** Creates a CommandFactory using commands registered by {@link CommandModule}. */
@@ -102,7 +102,7 @@
};
}
- private class Trampoline implements Command, SessionAware {
+ private class Trampoline implements Command, ServerSessionAware {
private final String commandLine;
private final String[] argv;
private InputStream in;
@@ -185,6 +185,12 @@
cmd.setExitCallback(
new ExitCallback() {
@Override
+ public void onExit(int rc, String exitMessage, boolean closeImmediately) {
+ exit.onExit(translateExit(rc), exitMessage, closeImmediately);
+ log(rc, exitMessage);
+ }
+
+ @Override
public void onExit(int rc, String exitMessage) {
exit.onExit(translateExit(rc), exitMessage);
log(rc, exitMessage);
diff --git a/java/com/google/gerrit/sshd/DispatchCommandProvider.java b/java/com/google/gerrit/sshd/DispatchCommandProvider.java
index 2a65ed0..acf2df9 100644
--- a/java/com/google/gerrit/sshd/DispatchCommandProvider.java
+++ b/java/com/google/gerrit/sshd/DispatchCommandProvider.java
@@ -91,7 +91,7 @@
return m;
}
- private static final TypeLiteral<Command> type = new TypeLiteral<Command>() {};
+ private static final TypeLiteral<Command> type = new TypeLiteral<>() {};
private List<Binding<Command>> allCommands() {
return injector.findBindingsByType(type);
diff --git a/java/com/google/gerrit/sshd/NoShell.java b/java/com/google/gerrit/sshd/NoShell.java
index e3f654b..ffac946 100644
--- a/java/com/google/gerrit/sshd/NoShell.java
+++ b/java/com/google/gerrit/sshd/NoShell.java
@@ -32,11 +32,11 @@
import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
import org.apache.sshd.server.Environment;
import org.apache.sshd.server.ExitCallback;
-import org.apache.sshd.server.SessionAware;
import org.apache.sshd.server.channel.ChannelSession;
import org.apache.sshd.server.command.AsyncCommand;
import org.apache.sshd.server.command.Command;
import org.apache.sshd.server.session.ServerSession;
+import org.apache.sshd.server.session.ServerSessionAware;
import org.apache.sshd.server.shell.ShellFactory;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.util.SystemReader;
@@ -65,7 +65,7 @@
*
* @see org.apache.sshd.server.command.AsyncCommand
*/
- static class SendMessage implements AsyncCommand, SessionAware {
+ static class SendMessage implements AsyncCommand, ServerSessionAware {
private final Provider<MessageFactory> messageFactory;
private final SshScope sshScope;
diff --git a/java/com/google/gerrit/sshd/SshLogJsonLayout.java b/java/com/google/gerrit/sshd/SshLogJsonLayout.java
index fca0a5a..0edad6f 100644
--- a/java/com/google/gerrit/sshd/SshLogJsonLayout.java
+++ b/java/com/google/gerrit/sshd/SshLogJsonLayout.java
@@ -26,11 +26,14 @@
import static com.google.gerrit.sshd.SshLog.P_USER_NAME;
import static com.google.gerrit.sshd.SshLog.P_WAIT;
+import com.google.common.base.Splitter;
import com.google.gerrit.util.logging.JsonLayout;
import com.google.gerrit.util.logging.JsonLogEntry;
+import java.util.List;
import org.apache.log4j.spi.LoggingEvent;
public class SshLogJsonLayout extends JsonLayout {
+ private static final Splitter SPLITTER = Splitter.on(" ");
@Override
public JsonLogEntry toJsonLogEntry(LoggingEvent event) {
@@ -81,18 +84,18 @@
String metricString = getMdcString(event, P_MESSAGE);
if (metricString != null && !metricString.isEmpty()) {
- String[] ssh_metrics = metricString.split(" ");
- this.timeNegotiating = ssh_metrics[0];
- this.timeSearchReuse = ssh_metrics[1];
- this.timeSearchSizes = ssh_metrics[2];
- this.timeCounting = ssh_metrics[3];
- this.timeCompressing = ssh_metrics[4];
- this.timeWriting = ssh_metrics[5];
- this.timeTotal = ssh_metrics[6];
- this.bitmapIndexMisses = ssh_metrics[7];
- this.deltasTotal = ssh_metrics[8];
- this.objectsTotal = ssh_metrics[9];
- this.bytesTotal = ssh_metrics[10];
+ List<String> ssh_metrics = SPLITTER.splitToList(" ");
+ this.timeNegotiating = ssh_metrics.get(0);
+ this.timeSearchReuse = ssh_metrics.get(1);
+ this.timeSearchSizes = ssh_metrics.get(2);
+ this.timeCounting = ssh_metrics.get(3);
+ this.timeCompressing = ssh_metrics.get(4);
+ this.timeWriting = ssh_metrics.get(5);
+ this.timeTotal = ssh_metrics.get(6);
+ this.bitmapIndexMisses = ssh_metrics.get(7);
+ this.deltasTotal = ssh_metrics.get(8);
+ this.objectsTotal = ssh_metrics.get(9);
+ this.bytesTotal = ssh_metrics.get(10);
}
}
}
diff --git a/java/com/google/gerrit/sshd/SshLogLayout.java b/java/com/google/gerrit/sshd/SshLogLayout.java
index a1f2c40..bb7edfa 100644
--- a/java/com/google/gerrit/sshd/SshLogLayout.java
+++ b/java/com/google/gerrit/sshd/SshLogLayout.java
@@ -32,7 +32,7 @@
import org.eclipse.jgit.util.QuotedString;
public final class SshLogLayout extends Layout {
- protected final LogTimestampFormatter timestampFormatter;
+ private final LogTimestampFormatter timestampFormatter;
public SshLogLayout() {
timestampFormatter = new LogTimestampFormatter();
@@ -40,7 +40,7 @@
@Override
public String format(LoggingEvent event) {
- final StringBuffer buf = new StringBuffer(128);
+ final StringBuilder buf = new StringBuilder(128);
buf.append('[');
buf.append(timestampFormatter.format(event.getTimeStamp()));
@@ -75,7 +75,7 @@
return buf.toString();
}
- private void req(String key, StringBuffer buf, LoggingEvent event) {
+ private void req(String key, StringBuilder buf, LoggingEvent event) {
Object val = event.getMDC(key);
buf.append(' ');
if (val != null) {
diff --git a/java/com/google/gerrit/sshd/SshScope.java b/java/com/google/gerrit/sshd/SshScope.java
index e9ed750..0fe8b78 100644
--- a/java/com/google/gerrit/sshd/SshScope.java
+++ b/java/com/google/gerrit/sshd/SshScope.java
@@ -219,7 +219,7 @@
new Scope() {
@Override
public <T> Provider<T> scope(Key<T> key, Provider<T> creator) {
- return new Provider<T>() {
+ return new Provider<>() {
@Override
public T get() {
return requireContext().get(key, creator);
diff --git a/java/com/google/gerrit/sshd/SshSessionFactoryInitializer.java b/java/com/google/gerrit/sshd/SshSessionFactoryInitializer.java
index 1cdf923..de91b68 100644
--- a/java/com/google/gerrit/sshd/SshSessionFactoryInitializer.java
+++ b/java/com/google/gerrit/sshd/SshSessionFactoryInitializer.java
@@ -14,9 +14,6 @@
package com.google.gerrit.sshd;
-import static com.google.gerrit.server.config.SshClientImplementation.APACHE;
-
-import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.transport.SshSessionFactory;
import org.eclipse.jgit.transport.sshd.DefaultProxyDataFactory;
import org.eclipse.jgit.transport.sshd.JGitKeyCache;
@@ -24,13 +21,11 @@
import org.eclipse.jgit.util.FS;
public class SshSessionFactoryInitializer {
- public static void init(Config config) {
- if (APACHE == config.getEnum("ssh", null, "clientImplementation", APACHE)) {
- SshdSessionFactory factory =
- new SshdSessionFactory(new JGitKeyCache(), new DefaultProxyDataFactory());
- factory.setHomeDirectory(FS.DETECTED.userHome());
- SshSessionFactory.setInstance(factory);
- }
+ public static void init() {
+ SshdSessionFactory factory =
+ new SshdSessionFactory(new JGitKeyCache(), new DefaultProxyDataFactory());
+ factory.setHomeDirectory(FS.DETECTED.userHome());
+ SshSessionFactory.setInstance(factory);
}
private SshSessionFactoryInitializer() {}
diff --git a/java/com/google/gerrit/sshd/commands/ExternalIdCaseSensitivityMigrationCommand.java b/java/com/google/gerrit/sshd/commands/ExternalIdCaseSensitivityMigrationCommand.java
index 29cc1cf..aa147f0 100644
--- a/java/com/google/gerrit/sshd/commands/ExternalIdCaseSensitivityMigrationCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ExternalIdCaseSensitivityMigrationCommand.java
@@ -41,10 +41,13 @@
if (!isUserNameCaseInsensitive || !isUserNameCaseInsensitiveMigrationMode) {
die(
- "External IDs online migration requires auth.userNameCaseInsensitive and auth.userNameCaseInsensitiveMigrationMode to be set to true. Cannot start migration!");
+ "External IDs online migration requires auth.userNameCaseInsensitive and"
+ + " auth.userNameCaseInsensitiveMigrationMode to be set to true. Cannot start"
+ + " migration!");
}
onlineExternalIdCaseSensivityMigrator.migrate();
stdout.println(
- "External ids case insensitivity migration started. To check if it's completed look for \"External IDs migration completed!\" message in the Gerrit server logs");
+ "External ids case insensitivity migration started. To check if it's completed look for"
+ + " \"External IDs migration completed!\" message in the Gerrit server logs");
}
}
diff --git a/java/com/google/gerrit/sshd/commands/ListLoggingLevelCommand.java b/java/com/google/gerrit/sshd/commands/ListLoggingLevelCommand.java
index 1a7be32..742536c 100644
--- a/java/com/google/gerrit/sshd/commands/ListLoggingLevelCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ListLoggingLevelCommand.java
@@ -20,7 +20,7 @@
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
-import java.util.Enumeration;
+import java.util.Collections;
import java.util.Map;
import java.util.TreeMap;
import org.apache.log4j.LogManager;
@@ -37,19 +37,22 @@
@Argument(index = 0, required = false, metaVar = "NAME", usage = "used to match loggers")
private String name;
- @SuppressWarnings("unchecked")
@Override
protected void run() {
enableGracefulStop();
Map<String, String> logs = new TreeMap<>();
- for (Enumeration<Logger> logger = LogManager.getCurrentLoggers(); logger.hasMoreElements(); ) {
- Logger log = logger.nextElement();
- if (name == null || log.getName().contains(name)) {
- logs.put(log.getName(), log.getEffectiveLevel().toString());
+ for (Logger logger : getCurrentLoggers()) {
+ if (name == null || logger.getName().contains(name)) {
+ logs.put(logger.getName(), logger.getEffectiveLevel().toString());
}
}
for (Map.Entry<String, String> e : logs.entrySet()) {
stdout.println(e.getKey() + ": " + e.getValue());
}
}
+
+ @SuppressWarnings({"unchecked", "JdkObsolete"})
+ private static Iterable<Logger> getCurrentLoggers() {
+ return Collections.list(LogManager.getCurrentLoggers());
+ }
}
diff --git a/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java b/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
index 3faf598..4d16da6 100644
--- a/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
@@ -23,7 +23,7 @@
import com.google.gerrit.sshd.SshCommand;
import java.net.MalformedURLException;
import java.net.URL;
-import java.util.Enumeration;
+import java.util.Collections;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
@@ -58,27 +58,23 @@
@Argument(index = 1, required = false, metaVar = "NAME", usage = "used to match loggers")
private String name;
- @SuppressWarnings("unchecked")
@Override
protected void run() throws MalformedURLException {
enableGracefulStop();
if (level == LevelOption.RESET) {
reset();
} else {
- for (Enumeration<Logger> logger = LogManager.getCurrentLoggers();
- logger.hasMoreElements(); ) {
- Logger log = logger.nextElement();
- if (name == null || log.getName().contains(name)) {
- log.setLevel(Level.toLevel(level.name()));
+ for (Logger logger : getCurrentLoggers()) {
+ if (name == null || logger.getName().contains(name)) {
+ logger.setLevel(Level.toLevel(level.name()));
}
}
}
}
- @SuppressWarnings("unchecked")
private static void reset() throws MalformedURLException {
- for (Enumeration<Logger> logger = LogManager.getCurrentLoggers(); logger.hasMoreElements(); ) {
- logger.nextElement().setLevel(null);
+ for (Logger logger : getCurrentLoggers()) {
+ logger.setLevel(null);
}
String path = System.getProperty(JAVA_OPTIONS_LOG_CONFIG);
@@ -88,4 +84,9 @@
PropertyConfigurator.configure(new URL(path));
}
}
+
+ @SuppressWarnings({"unchecked", "JdkObsolete"})
+ private static Iterable<Logger> getCurrentLoggers() {
+ return Collections.list(LogManager.getCurrentLoggers());
+ }
}
diff --git a/java/com/google/gerrit/sshd/commands/SetTopicCommand.java b/java/com/google/gerrit/sshd/commands/SetTopicCommand.java
index 35cb3ba..244fdbe 100644
--- a/java/com/google/gerrit/sshd/commands/SetTopicCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetTopicCommand.java
@@ -84,8 +84,7 @@
for (ChangeResource r : changes.values()) {
SetTopicOp op = topicOpFactory.create(topic);
- try (BatchUpdate u =
- updateFactory.create(r.getChange().getProject(), user, TimeUtil.nowTs())) {
+ try (BatchUpdate u = updateFactory.create(r.getChange().getProject(), user, TimeUtil.now())) {
u.addOp(r.getId(), op);
u.execute();
}
diff --git a/java/com/google/gerrit/sshd/commands/ShowCaches.java b/java/com/google/gerrit/sshd/commands/ShowCaches.java
index 02956f7..5b89228 100644
--- a/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -43,9 +43,10 @@
import com.google.gerrit.sshd.SshDaemon;
import com.google.inject.Inject;
import java.io.IOException;
-import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
import java.util.Collection;
-import java.util.Date;
import java.util.Map;
import org.apache.sshd.common.io.IoAcceptor;
import org.apache.sshd.common.io.IoSession;
@@ -114,13 +115,16 @@
protected void run() throws Failure {
enableGracefulStop();
nw = columns - 50;
- Date now = new Date();
+ Instant now = Instant.now();
+ DateTimeFormatter fmt =
+ DateTimeFormatter.ofPattern("HH:mm:ss zzz").withZone(ZoneId.of("UTC"));
stdout.format(
"%-25s %-20s now %16s\n",
"Gerrit Code Review",
Version.getVersion() != null ? Version.getVersion() : "",
- new SimpleDateFormat("HH:mm:ss zzz").format(now));
- stdout.format("%-25s %-20s uptime %16s\n", "", "", uptime(now.getTime() - serverStarted));
+ fmt.format(now));
+ stdout.format(
+ "%-25s %-20s uptime %16s\n", "", "", uptime(now.toEpochMilli() - serverStarted));
stdout.print('\n');
try {
diff --git a/java/com/google/gerrit/sshd/commands/ShowConnections.java b/java/com/google/gerrit/sshd/commands/ShowConnections.java
index 7eeb770..5efeb42 100644
--- a/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -34,8 +34,9 @@
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
-import java.text.SimpleDateFormat;
-import java.util.Date;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
import java.util.Optional;
import org.apache.sshd.common.io.IoAcceptor;
import org.apache.sshd.common.io.IoSession;
@@ -171,10 +172,13 @@
}
private static String time(long now, long time) {
+ Instant instant = Instant.ofEpochMilli(time);
if (now - time < 24 * 60 * 60 * 1000L) {
- return new SimpleDateFormat("HH:mm:ss").format(new Date(time));
+ return DateTimeFormatter.ofPattern("HH:mm:ss")
+ .withZone(ZoneId.systemDefault())
+ .format(instant);
}
- return new SimpleDateFormat("MMM-dd").format(new Date(time));
+ return DateTimeFormatter.ofPattern("MMM-dd").withZone(ZoneId.systemDefault()).format(instant);
}
private static String age(long age) {
diff --git a/java/com/google/gerrit/sshd/commands/ShowQueue.java b/java/com/google/gerrit/sshd/commands/ShowQueue.java
index 779f2df..4254e5b 100644
--- a/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -35,8 +35,9 @@
import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
import java.io.IOException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import org.apache.sshd.server.Environment;
@@ -154,7 +155,7 @@
stdout.print(
String.format(
"%8s %-12s %-12s %-4s %s\n",
- task.id, start, startTime(task.startTime), "", command));
+ task.id, start, startTime(task.startTime.toInstant()), "", command));
} else {
String remoteName =
task.remoteName != null ? task.remoteName + "/" + task.projectName : task.projectName;
@@ -164,7 +165,7 @@
"%8s %-12s %-4s %s\n",
task.id,
start,
- startTime(task.startTime),
+ startTime(task.startTime.toInstant()),
MoreObjects.firstNonNull(remoteName, "n/a")));
}
}
@@ -178,19 +179,23 @@
}
private static String time(long now, long delay) {
- Date when = new Date(now + delay);
+ Instant when = Instant.ofEpochMilli(now + delay);
return format(when, delay);
}
- private static String startTime(Date when) {
- return format(when, TimeUtil.nowMs() - when.getTime());
+ private static String startTime(Instant when) {
+ return format(when, TimeUtil.nowMs() - when.toEpochMilli());
}
- private static String format(Date when, long timeFromNow) {
+ private static String format(Instant when, long timeFromNow) {
if (timeFromNow < 24 * 60 * 60 * 1000L) {
- return new SimpleDateFormat("HH:mm:ss.SSS").format(when);
+ return DateTimeFormatter.ofPattern("HH:mm:ss.SSS")
+ .withZone(ZoneId.systemDefault())
+ .format(when);
}
- return new SimpleDateFormat("MMM-dd HH:mm").format(when);
+ return DateTimeFormatter.ofPattern("MMM-dd HH:mm")
+ .withZone(ZoneId.systemDefault())
+ .format(when);
}
private static String format(Task.State state) {
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index be32138..798a2d4 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -48,8 +48,6 @@
"//lib/flogger:api",
"//lib/guice",
"//lib/guice:guice-servlet",
- "//lib/log:impl-log4j",
- "//lib/log:log4j",
"//lib/truth",
],
)
diff --git a/java/com/google/gerrit/testing/FakeAccountCache.java b/java/com/google/gerrit/testing/FakeAccountCache.java
index ab3348b..49a8d71 100644
--- a/java/com/google/gerrit/testing/FakeAccountCache.java
+++ b/java/com/google/gerrit/testing/FakeAccountCache.java
@@ -40,7 +40,7 @@
return state;
}
return newState(
- Account.builder(accountId, TimeUtil.nowTs())
+ Account.builder(accountId, TimeUtil.now())
.setMetaId("1234567812345678123456781234567812345678")
.build());
}
diff --git a/java/com/google/gerrit/testing/FloggerInitializer.java b/java/com/google/gerrit/testing/FloggerInitializer.java
index 1972107..7793de1 100644
--- a/java/com/google/gerrit/testing/FloggerInitializer.java
+++ b/java/com/google/gerrit/testing/FloggerInitializer.java
@@ -25,7 +25,7 @@
public static void initBackend() {
System.setProperty(
FLOGGER_BACKEND_PROPERTY,
- "com.google.common.flogger.backend.log4j.Log4jBackendFactory#getInstance");
+ "com.google.common.flogger.backend.system.SimpleBackendFactory#getInstance");
System.setProperty(FLOGGER_LOGGING_CONTEXT, LoggingContext.class.getName() + "#getInstance");
}
}
diff --git a/java/com/google/gerrit/testing/InMemoryRepositoryManager.java b/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
index 362e23c..2051ae3 100644
--- a/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
+++ b/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
@@ -14,16 +14,16 @@
package com.google.gerrit.testing;
-import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Sets;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.Project.NameKey;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.RepositoryCaseMismatchException;
import com.google.inject.Inject;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
-import java.util.SortedSet;
+import java.util.NavigableSet;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.internal.storage.dfs.DfsRepository;
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
@@ -111,12 +111,12 @@
}
@Override
- public synchronized SortedSet<Project.NameKey> list() {
- SortedSet<Project.NameKey> names = Sets.newTreeSet();
+ public synchronized NavigableSet<Project.NameKey> list() {
+ NavigableSet<Project.NameKey> names = Sets.newTreeSet();
for (DfsRepository repo : repos.values()) {
names.add(Project.nameKey(repo.getDescription().getRepositoryName()));
}
- return ImmutableSortedSet.copyOf(names);
+ return Collections.unmodifiableNavigableSet(names);
}
public synchronized void deleteRepository(Project.NameKey name) {
diff --git a/java/com/google/gerrit/testing/IndexVersions.java b/java/com/google/gerrit/testing/IndexVersions.java
index 3281ffc..f245665 100644
--- a/java/com/google/gerrit/testing/IndexVersions.java
+++ b/java/com/google/gerrit/testing/IndexVersions.java
@@ -27,7 +27,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
-import java.util.SortedMap;
+import java.util.NavigableMap;
import org.eclipse.jgit.lib.Config;
public class IndexVersions {
@@ -90,7 +90,7 @@
value = value.trim();
}
- SortedMap<Integer, Schema<V>> schemas = schemaDef.getSchemas();
+ NavigableMap<Integer, Schema<V>> schemas = schemaDef.getSchemas();
if (!Strings.isNullOrEmpty(value)) {
if (ALL.equals(value)) {
return ImmutableList.copyOf(schemas.keySet());
diff --git a/java/com/google/gerrit/testing/TestChanges.java b/java/com/google/gerrit/testing/TestChanges.java
index b795c5b..8bd02b8 100644
--- a/java/com/google/gerrit/testing/TestChanges.java
+++ b/java/com/google/gerrit/testing/TestChanges.java
@@ -58,7 +58,7 @@
changeId,
userId,
BranchNameKey.create(project, "master"),
- TimeUtil.nowTs());
+ TimeUtil.now());
incrementPatchSet(c);
return c;
}
@@ -72,7 +72,7 @@
.id(id)
.commitId(ObjectId.fromString(revision))
.uploader(userId)
- .createdOn(TimeUtil.nowTs())
+ .createdOn(TimeUtil.now())
.build();
}
@@ -94,7 +94,7 @@
injector.getInstance(AbstractChangeNotes.Args.class), c, shouldExist, null)
.load(),
user,
- TimeUtil.nowTs(),
+ TimeUtil.now(),
Ordering.natural());
ChangeNotes notes = update.getNotes();
diff --git a/java/com/google/gerrit/testing/TestCommentHelper.java b/java/com/google/gerrit/testing/TestCommentHelper.java
index 5865a3c..400b559 100644
--- a/java/com/google/gerrit/testing/TestCommentHelper.java
+++ b/java/com/google/gerrit/testing/TestCommentHelper.java
@@ -33,6 +33,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
/** Test helper for dealing with comments/drafts. */
public class TestCommentHelper {
@@ -64,7 +65,7 @@
gApi.changes().id(changeId).current().createDraft(in);
}
- public Collection<CommentInfo> getPublishedComments(String changeId) throws Exception {
+ public List<CommentInfo> getPublishedComments(String changeId) throws Exception {
return gApi.changes().id(changeId).commentsRequest().get().values().stream()
.flatMap(Collection::stream)
.collect(toList());
diff --git a/java/com/google/gerrit/testing/TestLoggingActivator.java b/java/com/google/gerrit/testing/TestLoggingActivator.java
index ee1a525..c7e22c9 100644
--- a/java/com/google/gerrit/testing/TestLoggingActivator.java
+++ b/java/com/google/gerrit/testing/TestLoggingActivator.java
@@ -14,15 +14,12 @@
package com.google.gerrit.testing;
-import static org.apache.log4j.Logger.getLogger;
-
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
-import org.apache.log4j.ConsoleAppender;
-import org.apache.log4j.Level;
-import org.apache.log4j.LogManager;
-import org.apache.log4j.Logger;
-import org.apache.log4j.PatternLayout;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.Logger;
public class TestLoggingActivator {
private static final ImmutableMap<String, Level> LOG_LEVELS =
@@ -30,38 +27,38 @@
.put("com.google.gerrit", getGerritLogLevel())
// Silence non-critical messages from MINA SSHD.
- .put("org.apache.mina", Level.WARN)
- .put("org.apache.sshd.client", Level.WARN)
- .put("org.apache.sshd.common", Level.WARN)
- .put("org.apache.sshd.server", Level.WARN)
+ .put("org.apache.mina", Level.WARNING)
+ .put("org.apache.sshd.client", Level.WARNING)
+ .put("org.apache.sshd.common", Level.WARNING)
+ .put("org.apache.sshd.server", Level.WARNING)
.put("org.apache.sshd.common.keyprovider.FileKeyPairProvider", Level.INFO)
- .put("com.google.gerrit.sshd.GerritServerSession", Level.WARN)
+ .put("com.google.gerrit.sshd.GerritServerSession", Level.WARNING)
// Silence non-critical messages from mime-util.
- .put("eu.medsea.mimeutil", Level.WARN)
+ .put("eu.medsea.mimeutil", Level.WARNING)
// Silence non-critical messages from openid4java.
- .put("org.apache.xml", Level.WARN)
- .put("org.openid4java", Level.WARN)
- .put("org.openid4java.consumer.ConsumerManager", Level.FATAL)
- .put("org.openid4java.discovery.Discovery", Level.ERROR)
- .put("org.openid4java.server.RealmVerifier", Level.ERROR)
- .put("org.openid4java.message.AuthSuccess", Level.ERROR)
+ .put("org.apache.xml", Level.WARNING)
+ .put("org.openid4java", Level.WARNING)
+ .put("org.openid4java.consumer.ConsumerManager", Level.SEVERE)
+ .put("org.openid4java.discovery.Discovery", Level.SEVERE)
+ .put("org.openid4java.server.RealmVerifier", Level.SEVERE)
+ .put("org.openid4java.message.AuthSuccess", Level.SEVERE)
// Silence non-critical messages from apache.http.
- .put("org.apache.http", Level.WARN)
+ .put("org.apache.http", Level.WARNING)
// Silence non-critical messages from Jetty.
- .put("org.eclipse.jetty", Level.WARN)
+ .put("org.eclipse.jetty", Level.WARNING)
// Silence non-critical messages from JGit.
- .put("org.eclipse.jgit.transport.PacketLineIn", Level.WARN)
- .put("org.eclipse.jgit.transport.PacketLineOut", Level.WARN)
- .put("org.eclipse.jgit.internal.transport.sshd", Level.WARN)
- .put("org.eclipse.jgit.util.FileUtils", Level.WARN)
- .put("org.eclipse.jgit.internal.storage.file.FileSnapshot", Level.WARN)
- .put("org.eclipse.jgit.util.FS", Level.WARN)
- .put("org.eclipse.jgit.util.SystemReader", Level.WARN)
+ .put("org.eclipse.jgit.transport.PacketLineIn", Level.WARNING)
+ .put("org.eclipse.jgit.transport.PacketLineOut", Level.WARNING)
+ .put("org.eclipse.jgit.internal.transport.sshd", Level.WARNING)
+ .put("org.eclipse.jgit.util.FileUtils", Level.WARNING)
+ .put("org.eclipse.jgit.internal.storage.file.FileSnapshot", Level.WARNING)
+ .put("org.eclipse.jgit.util.FS", Level.WARNING)
+ .put("org.eclipse.jgit.util.SystemReader", Level.WARNING)
.build();
private static Level getGerritLogLevel() {
@@ -69,27 +66,44 @@
if (value.isEmpty()) {
value = Strings.nullToEmpty(System.getProperty("gerrit.logLevel"));
}
- return Level.toLevel(value, Level.INFO);
+
+ try {
+ return Level.parse(value);
+ } catch (IllegalArgumentException e) {
+ // for backwards compatibility handle log4j log levels
+ if (value.equalsIgnoreCase("FATAL") || value.equalsIgnoreCase("ERROR")) {
+ return Level.SEVERE;
+ }
+ if (value.equalsIgnoreCase("WARN")) {
+ return Level.WARNING;
+ }
+ if (value.equalsIgnoreCase("DEBUG")) {
+ return Level.FINE;
+ }
+ if (value.equalsIgnoreCase("TRACE")) {
+ return Level.FINEST;
+ }
+
+ return Level.INFO;
+ }
}
public static void configureLogging() {
- LogManager.resetConfiguration();
+ LogManager.getLogManager().reset();
FloggerInitializer.initBackend();
- PatternLayout layout = new PatternLayout();
- layout.setConversionPattern("%-5p %c %x: %m%n");
+ ConsoleHandler dst = new ConsoleHandler();
+ dst.setLevel(Level.FINEST);
- ConsoleAppender dst = new ConsoleAppender();
- dst.setLayout(layout);
- dst.setTarget("System.err");
- dst.setThreshold(Level.DEBUG);
- dst.activateOptions();
+ Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).addHandler(dst);
- Logger root = LogManager.getRootLogger();
- root.removeAllAppenders();
- root.addAppender(dst);
-
- LOG_LEVELS.entrySet().stream().forEach(e -> getLogger(e.getKey()).setLevel(e.getValue()));
+ LOG_LEVELS.entrySet().stream()
+ .forEach(
+ e -> {
+ Logger logger = Logger.getLogger(e.getKey());
+ logger.setLevel(e.getValue());
+ logger.addHandler(dst);
+ });
}
private TestLoggingActivator() {}
diff --git a/java/com/google/gerrit/util/http/BUILD b/java/com/google/gerrit/util/http/BUILD
index fbd1379..afb4e25 100644
--- a/java/com/google/gerrit/util/http/BUILD
+++ b/java/com/google/gerrit/util/http/BUILD
@@ -4,5 +4,8 @@
name = "http",
srcs = glob(["**/*.java"]),
visibility = ["//visibility:public"],
- deps = ["//lib:servlet-api"],
+ deps = [
+ "//lib:guava",
+ "//lib:servlet-api",
+ ],
)
diff --git a/java/com/google/gerrit/util/http/RequestUtil.java b/java/com/google/gerrit/util/http/RequestUtil.java
index f64ce5a..51ffe0c 100644
--- a/java/com/google/gerrit/util/http/RequestUtil.java
+++ b/java/com/google/gerrit/util/http/RequestUtil.java
@@ -14,10 +14,14 @@
package com.google.gerrit.util.http;
+import com.google.common.base.Splitter;
+import java.util.List;
import javax.servlet.http.HttpServletRequest;
/** Utilities for manipulating HTTP request objects. */
public class RequestUtil {
+ private static final Splitter SPLITTER = Splitter.on("/");
+
/** HTTP request attribute for storing the Throwable that caused an error condition. */
private static final String ATTRIBUTE_ERROR_TRACE =
RequestUtil.class.getName() + "/ErrorTraceThrowable";
@@ -80,11 +84,11 @@
encodedPathInfo = encodedPathInfo.substring(2);
}
- String[] parts = encodedPathInfo.split("/");
- StringBuilder result = new StringBuilder(parts.length);
- for (int i = 0; i < parts.length; i = i + 2) {
+ List<String> parts = SPLITTER.splitToList(encodedPathInfo);
+ StringBuilder result = new StringBuilder(parts.size());
+ for (int i = 0; i < parts.size(); i = i + 2) {
result.append("/");
- result.append(parts[i]);
+ result.append(parts.get(i));
}
return result.toString();
}
diff --git a/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java b/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
index 7758be6..2ea2a55 100644
--- a/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
@@ -126,7 +126,7 @@
private static String getMessageId(FakeEmailSender sender) {
return ((StringEmailHeader)
- (Iterables.getOnlyElement(sender.getMessages()).headers().get("Message-ID")))
+ Iterables.getOnlyElement(sender.getMessages()).headers().get("Message-ID"))
.getString();
}
diff --git a/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java b/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
index 7d04558..877ccd5 100644
--- a/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
+++ b/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
@@ -36,6 +36,7 @@
import com.google.gerrit.testing.InMemoryRepositoryManager;
import com.google.gerrit.testing.TestTimeUtil;
import java.io.IOException;
+import java.util.Date;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Constants;
@@ -332,10 +333,13 @@
assertThat(repo.exactRef(ref.getName())).isNull();
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
private ObjectId createCommit(Repository repo) throws IOException {
try (ObjectInserter oi = repo.newObjectInserter()) {
PersonIdent ident =
- new PersonIdent(new PersonIdent("Foo Bar", "foo.bar@baz.com"), TimeUtil.nowTs());
+ new PersonIdent(new PersonIdent("Foo Bar", "foo.bar@baz.com"), Date.from(TimeUtil.now()));
CommitBuilder cb = new CommitBuilder();
cb.setTreeId(oi.insert(Constants.OBJ_TREE, new byte[] {}));
cb.setCommitter(ident);
diff --git a/javatests/com/google/gerrit/acceptance/annotation/UseClockStepTest.java b/javatests/com/google/gerrit/acceptance/annotation/UseClockStepTest.java
index ecfe3f5..9d689ba 100644
--- a/javatests/com/google/gerrit/acceptance/annotation/UseClockStepTest.java
+++ b/javatests/com/google/gerrit/acceptance/annotation/UseClockStepTest.java
@@ -19,7 +19,6 @@
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.server.util.time.TimeUtil;
-import java.sql.Timestamp;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
@@ -52,6 +51,6 @@
@Test
@UseClockStep(startAtEpoch = true)
public void useClockStepWithStartAtEpoch() {
- assertThat(TimeUtil.nowTs()).isEqualTo(Timestamp.from(Instant.EPOCH));
+ assertThat(TimeUtil.now()).isEqualTo(Instant.EPOCH);
}
}
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 915e759..3678e25 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -158,6 +158,7 @@
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -499,7 +500,7 @@
assertThat(ref).isNotNull();
RevCommit c = rw.parseCommit(ref.getObjectId());
long timestampDiffMs =
- Math.abs(c.getCommitTime() * 1000L - getAccount(accountId).registeredOn().getTime());
+ Math.abs(c.getCommitTime() * 1000L - getAccount(accountId).registeredOn().toEpochMilli());
assertThat(timestampDiffMs).isAtMost(SECONDS.toMillis(1));
// Check the 'account.config' file.
@@ -980,7 +981,8 @@
assertThat(detail.email).isEqualTo(email);
assertThat(detail.secondaryEmails).containsExactly(secondaryEmail);
assertThat(detail.status).isEqualTo(status);
- assertThat(detail.registeredOn).isEqualTo(getAccount(foo.id()).registeredOn());
+ assertThat(detail.registeredOn.getTime())
+ .isEqualTo(getAccount(foo.id()).registeredOn().toEpochMilli());
assertThat(detail.inactive).isNull();
assertThat(detail._moreAccounts).isNull();
}
@@ -2464,6 +2466,9 @@
}
@Test
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
public void stalenessChecker() throws Exception {
// Newly created account is not stale.
AccountInfo accountInfo = gApi.accounts().create(name("foo")).get();
@@ -2477,7 +2482,7 @@
RevWalk rw = new RevWalk(repo)) {
RevCommit commit = rw.parseCommit(repo.exactRef(userRef).getObjectId());
- PersonIdent ident = new PersonIdent(serverIdent.get(), TimeUtil.nowTs());
+ PersonIdent ident = new PersonIdent(serverIdent.get(), Date.from(TimeUtil.now()));
CommitBuilder cb = new CommitBuilder();
cb.setTreeId(commit.getTree());
cb.setCommitter(ident);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 0d7210f..66dbe80 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -15,6 +15,7 @@
package com.google.gerrit.acceptance.api.change;
import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.Truth8.assertThat;
@@ -31,6 +32,7 @@
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
import static com.google.gerrit.entities.RefNames.changeMetaRef;
+import static com.google.gerrit.extensions.client.ChangeStatus.MERGED;
import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.CHECK;
@@ -87,8 +89,10 @@
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
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.common.FooterConstants;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.entities.Account;
@@ -130,6 +134,7 @@
import com.google.gerrit.extensions.api.changes.ReviewerResult;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.api.groups.GroupApi;
+import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.api.projects.ConfigInput;
import com.google.gerrit.extensions.api.projects.ProjectInput;
@@ -149,10 +154,12 @@
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.GitPerson;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.LegacySubmitRequirementInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.common.SubmitRecordInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementExpressionInfo;
import com.google.gerrit.extensions.common.SubmitRequirementInput;
import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
import com.google.gerrit.extensions.common.SubmitRequirementResultInfo.Status;
@@ -185,6 +192,7 @@
import com.google.gerrit.server.patch.DiffSummaryKey;
import com.google.gerrit.server.patch.IntraLineDiff;
import com.google.gerrit.server.patch.IntraLineDiffKey;
+import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.testing.TestLabels;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder.ChangeOperatorFactory;
@@ -203,6 +211,7 @@
import java.io.IOException;
import java.sql.Timestamp;
import java.text.MessageFormat;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -214,18 +223,21 @@
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
+import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.PushResult;
-import org.junit.Ignore;
+import org.eclipse.jgit.util.RawParseUtils;
import org.junit.Test;
@NoHttpd
@@ -1925,7 +1937,7 @@
PushOneCommit.Result r = createChange();
ChangeResource rsrc = parseResource(r);
String oldETag = rsrc.getETag();
- Timestamp oldTs = rsrc.getChange().getLastUpdatedOn();
+ Instant oldTs = rsrc.getChange().getLastUpdatedOn();
addReviewer.call(r.getChangeId(), user.email());
@@ -2036,7 +2048,7 @@
PushOneCommit.Result r = createChange();
ChangeResource rsrc = parseResource(r);
String oldETag = rsrc.getETag();
- Timestamp oldTs = rsrc.getChange().getLastUpdatedOn();
+ Instant oldTs = rsrc.getChange().getLastUpdatedOn();
// create a group named "ab" with one user: testUser
String email = "abcd@example.com";
@@ -2085,7 +2097,7 @@
PushOneCommit.Result r = createChange();
ChangeResource rsrc = parseResource(r);
String oldETag = rsrc.getETag();
- Timestamp oldTs = rsrc.getChange().getLastUpdatedOn();
+ Instant oldTs = rsrc.getChange().getLastUpdatedOn();
// create a group named "kobe" with one user: lee
String testUserFullname = "kobebryant";
@@ -2186,7 +2198,7 @@
PushOneCommit.Result r = createChange();
ChangeResource rsrc = parseResource(r);
String oldETag = rsrc.getETag();
- Timestamp oldTs = rsrc.getChange().getLastUpdatedOn();
+ Instant oldTs = rsrc.getChange().getLastUpdatedOn();
ReviewerInput in = new ReviewerInput();
in.reviewer = user.email();
@@ -2830,6 +2842,53 @@
}
@Test
+ public void labelPermissionsChange_doesNotAffectCurrentVotes() throws Exception {
+ String heads = "refs/heads/*";
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allowLabel(LabelId.CODE_REVIEW).ref(heads).group(REGISTERED_USERS).range(-2, +2))
+ .update();
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ // Approve the change as user
+ requestScopeOperations.setApiUser(user.id());
+ approve(changeId);
+ assertThat(
+ gApi.changes().id(changeId).get(DETAILED_LABELS).labels.get("Code-Review").all.stream()
+ .collect(toImmutableMap(vote -> Account.id(vote._accountId), vote -> vote.value)))
+ .isEqualTo(ImmutableMap.of(user.id(), 2));
+
+ // Remove permissions for CODE_REVIEW. The user still has [-1,+1], inherited from All-Projects.
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .remove(labelPermissionKey(LabelId.CODE_REVIEW).ref(heads).group(REGISTERED_USERS))
+ .update();
+
+ // No permissions to vote +2
+ assertThrows(AuthException.class, () -> approve(changeId));
+
+ assertThat(
+ get(changeId, DETAILED_LABELS).labels.get(LabelId.CODE_REVIEW).all.stream()
+ .map(vote -> vote.value))
+ .containsExactly(2);
+
+ // The change is still submittable
+ requestScopeOperations.setApiUser(admin.id());
+ gApi.changes().id(changeId).current().submit();
+ assertThat(info(changeId).status).isEqualTo(MERGED);
+
+ // The +2 vote out of permissions range is still present.
+ assertThat(
+ get(changeId, DETAILED_LABELS).labels.get(LabelId.CODE_REVIEW).all.stream()
+ .collect(toImmutableMap(vote -> Account.id(vote._accountId), vote -> vote.value)))
+ .isEqualTo(ImmutableMap.of(user.id(), 2, admin.id(), 0));
+ }
+
+ @Test
public void createEmptyChange() throws Exception {
ChangeInput in = new ChangeInput();
in.branch = Constants.MASTER;
@@ -3081,7 +3140,7 @@
gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
gApi.changes().id(r.getChangeId()).current().submit();
- assertThat(gApi.changes().id(r.getChangeId()).info().status).isEqualTo(ChangeStatus.MERGED);
+ assertThat(gApi.changes().id(r.getChangeId()).info().status).isEqualTo(MERGED);
}
@Test
@@ -3107,7 +3166,7 @@
.update();
requestScopeOperations.setApiUser(user.id());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
- assertThat(gApi.changes().id(r.getChangeId()).info().status).isEqualTo(ChangeStatus.MERGED);
+ assertThat(gApi.changes().id(r.getChangeId()).info().status).isEqualTo(MERGED);
}
@Test
@@ -3353,7 +3412,7 @@
assertThat(commitPatchSetCreation.getShortMessage()).isEqualTo("Create patch set 2");
PersonIdent expectedAuthor =
changeNoteUtil.newAccountIdIdent(
- getAccount(admin.id()).id(), c.updated, serverIdent.get());
+ getAccount(admin.id()).id(), c.updated.toInstant(), serverIdent.get());
assertThat(commitPatchSetCreation.getAuthorIdent()).isEqualTo(expectedAuthor);
assertThat(commitPatchSetCreation.getCommitterIdent())
.isEqualTo(new PersonIdent(serverIdent.get(), c.updated));
@@ -3363,7 +3422,7 @@
assertThat(commitChangeCreation.getShortMessage()).isEqualTo("Create change");
expectedAuthor =
changeNoteUtil.newAccountIdIdent(
- getAccount(admin.id()).id(), c.created, serverIdent.get());
+ getAccount(admin.id()).id(), c.created.toInstant(), serverIdent.get());
assertThat(commitChangeCreation.getAuthorIdent()).isEqualTo(expectedAuthor);
assertThat(commitChangeCreation.getCommitterIdent())
.isEqualTo(new PersonIdent(serverIdent.get(), c.created));
@@ -3475,6 +3534,10 @@
r2.assertOkStatus();
}
+ private void assertLabelDescription(ChangeInfo changeInfo, String labelName, String description) {
+ assertThat(changeInfo.labels.get(labelName).description).isEqualTo(description);
+ }
+
@Test
public void checkLabelsForUnsubmittedChange() throws Exception {
PushOneCommit.Result r = createChange();
@@ -3504,6 +3567,7 @@
.containsExactly(LabelId.CODE_REVIEW, LabelId.VERIFIED);
assertPermitted(change, LabelId.CODE_REVIEW, -2, -1, 0, 1, 2);
assertPermitted(change, LabelId.VERIFIED, -1, 0, 1);
+ assertLabelDescription(change, LabelId.VERIFIED, TestLabels.VERIFIED_LABEL_DESCRIPTION);
// add an approval on the new label
gApi.changes()
@@ -3546,7 +3610,7 @@
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
ChangeInfo change = gApi.changes().id(r.getChangeId()).get();
- assertThat(change.status).isEqualTo(ChangeStatus.MERGED);
+ assertThat(change.status).isEqualTo(MERGED);
assertThat(change.submissionId).isNotNull();
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertThat(change.permittedLabels.keySet()).containsExactly(LabelId.CODE_REVIEW);
@@ -3573,9 +3637,10 @@
.containsExactly(LabelId.CODE_REVIEW, LabelId.VERIFIED);
assertPermitted(change, LabelId.CODE_REVIEW, 2);
assertPermitted(change, LabelId.VERIFIED, 0, 1);
+ assertLabelDescription(change, LabelId.VERIFIED, TestLabels.VERIFIED_LABEL_DESCRIPTION);
- // ignore the new label by Prolog submit rule and assert that the label is
- // no longer returned
+ // Ignore the new label by Prolog submit rule. Permitted ranges are still going to be
+ // returned for the label.
GitUtil.fetch(testRepo, RefNames.REFS_CONFIG + ":config");
testRepo.reset("config");
PushOneCommit push2 =
@@ -3589,11 +3654,10 @@
change = gApi.changes().id(r.getChangeId()).get();
assertPermitted(change, LabelId.CODE_REVIEW, 2);
- assertPermitted(change, LabelId.VERIFIED);
+ assertPermitted(change, LabelId.VERIFIED, 0, 1);
- // add an approval on the new label and assert that the label is now
- // returned although it is ignored by the Prolog submit rule and hence not
- // included in the submit records
+ // add an approval on the new label. The label can still be voted +1 although it is ignored
+ // in Prolog. 0 is not permitted because votes cannot be decreased.
gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
@@ -3602,7 +3666,7 @@
change = gApi.changes().id(r.getChangeId()).get();
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW, LabelId.VERIFIED);
assertPermitted(change, LabelId.CODE_REVIEW, 2);
- assertPermitted(change, LabelId.VERIFIED);
+ assertPermitted(change, LabelId.VERIFIED, 1);
// remove label and assert that it's no longer returned for existing
// changes, even if there is an approval for it
@@ -3710,7 +3774,7 @@
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
ChangeInfo change = gApi.changes().id(r.getChangeId()).get();
- assertThat(change.status).isEqualTo(ChangeStatus.MERGED);
+ assertThat(change.status).isEqualTo(MERGED);
assertThat(change.submissionId).isNotNull();
assertThat(change.labels.keySet())
.containsExactly(LabelId.CODE_REVIEW, "Non-Author-Code-Review");
@@ -3727,7 +3791,7 @@
result.assertOkStatus();
ChangeInfo change = gApi.changes().id(r.getChangeId()).get();
- assertThat(change.status).isEqualTo(ChangeStatus.MERGED);
+ assertThat(change.status).isEqualTo(MERGED);
assertThat(change.submissionId).isNotNull();
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertPermitted(change, LabelId.CODE_REVIEW, 0, 1, 2);
@@ -3744,11 +3808,11 @@
result.assertOkStatus();
ChangeInfo firstChange = gApi.changes().id(first.getChangeId()).get();
- assertThat(firstChange.status).isEqualTo(ChangeStatus.MERGED);
+ assertThat(firstChange.status).isEqualTo(MERGED);
assertThat(firstChange.submissionId).isNotNull();
ChangeInfo secondChange = gApi.changes().id(second.getChangeId()).get();
- assertThat(secondChange.status).isEqualTo(ChangeStatus.MERGED);
+ assertThat(secondChange.status).isEqualTo(MERGED);
assertThat(secondChange.submissionId).isNotNull();
assertThat(secondChange.submissionId).isEqualTo(firstChange.submissionId);
@@ -3904,7 +3968,9 @@
assertThat(thrown)
.hasMessageThat()
.contains("Failed to submit 1 change due to the following problems");
- assertThat(thrown).hasMessageThat().contains("needs All-Comments-Resolved");
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("submit requirement 'All-Comments-Resolved' is unsatisfied");
}
@Test
@@ -4059,7 +4125,7 @@
assertThat(testLabel.status).isEqualTo(SubmitRecordInfo.Label.Status.OK);
assertThat(testLabel.appliedBy).isNull();
- voteLabel(changeId, "code-review", 2);
+ voteLabel(changeId, "Code-Review", 2);
// Code review record is satisfied after voting +2
change = gApi.changes().id(changeId).get();
assertThat(change.submitRecords).hasSize(2);
@@ -4159,16 +4225,13 @@
}
@Test
- @GerritConfig(
- name = "experiments.enabled",
- value = ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS)
public void submitRequirement_withLabelEqualsMax() throws Exception {
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
+ .setName("Code-Review")
.setSubmittabilityExpression(
- SubmitRequirementExpression.create("label:code-review=MAX"))
+ SubmitRequirementExpression.create("label:Code-Review=MAX"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4178,19 +4241,16 @@
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
- voteLabel(changeId, "code-review", 2);
+ voteLabel(changeId, "Code-Review", 2);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
}
@Test
- @GerritConfig(
- name = "experiments.enabled",
- value = ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS)
public void submitRequirement_withLabelEqualsMax_fromNonUploader() throws Exception {
configLabel("my-label", LabelFunction.NO_OP); // label function has no effect
projectOperations
@@ -4232,16 +4292,13 @@
}
@Test
- @GerritConfig(
- name = "experiments.enabled",
- value = ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS)
public void submitRequirement_withLabelEqualsMinBlockingSubmission() throws Exception {
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
+ .setName("Code-Review")
.setSubmittabilityExpression(
- SubmitRequirementExpression.create("-label:code-review=MIN"))
+ SubmitRequirementExpression.create("-label:Code-Review=MIN"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4252,33 +4309,30 @@
assertThat(change.submitRequirements).hasSize(2);
// Requirement is satisfied because there are no votes
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
// Legacy requirement (coming from the label function definition) is not satisfied. We return
// both legacy and non-legacy requirements in this case since their statuses are not identical.
assertSubmitRequirementStatus(
change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
- voteLabel(changeId, "code-review", -1);
+ voteLabel(changeId, "Code-Review", -1);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(2);
// Requirement is still satisfied because -1 is not the max negative value
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
assertSubmitRequirementStatus(
change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
- voteLabel(changeId, "code-review", -2);
+ voteLabel(changeId, "Code-Review", -2);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
// Requirement is now unsatisfied because -2 is the max negative value
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
}
@Test
- @GerritConfig(
- name = "experiments.enabled",
- value = ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS)
public void submitRequirement_withMaxWithBlock_ignoringSelfApproval() throws Exception {
configLabel("my-label", LabelFunction.MAX_WITH_BLOCK);
projectOperations
@@ -4328,16 +4382,13 @@
}
@Test
- @GerritConfig(
- name = "experiments.enabled",
- value = ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS)
public void submitRequirement_withLabelEqualsAny() throws Exception {
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
+ .setName("Code-Review")
.setSubmittabilityExpression(
- SubmitRequirementExpression.create("label:code-review=ANY"))
+ SubmitRequirementExpression.create("label:Code-Review=ANY"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4347,36 +4398,33 @@
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
- voteLabel(changeId, "code-review", 1);
+ voteLabel(changeId, "Code-Review", 1);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(2);
// Legacy and non-legacy requirements have mismatching status. Both are returned from the API.
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
assertSubmitRequirementStatus(
change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
}
@Test
- @GerritConfig(
- name = "experiments.enabled",
- value = ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS)
public void submitRequirementIsSatisfied_whenSubmittabilityExpressionIsFulfilled()
throws Exception {
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
.setAllowOverrideInChildProjects(false)
.build());
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("verified")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:verified=+1"))
+ .setName("Verified")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Verified=+1"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4386,32 +4434,29 @@
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(2);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
assertSubmitRequirementStatus(
- change.submitRequirements, "verified", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Verified", Status.UNSATISFIED, /* isLegacy= */ false);
- voteLabel(changeId, "code-review", 2);
+ voteLabel(changeId, "Code-Review", 2);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(2);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
assertSubmitRequirementStatus(
- change.submitRequirements, "verified", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Verified", Status.UNSATISFIED, /* isLegacy= */ false);
}
@Test
- @GerritConfig(
- name = "experiments.enabled",
- value = ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS)
public void submitRequirementIsNotApplicable_whenApplicabilityExpressionIsNotFulfilled()
throws Exception {
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
+ .setName("Code-Review")
.setApplicabilityExpression(SubmitRequirementExpression.of("project:foo"))
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4421,15 +4466,12 @@
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(2);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.NOT_APPLICABLE, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.NOT_APPLICABLE, /* isLegacy= */ false);
assertSubmitRequirementStatus(
change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
}
@Test
- @GerritConfig(
- name = "experiments.enabled",
- value = ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS)
public void submitRequirementIsOverridden_whenOverrideExpressionIsFulfilled() throws Exception {
configLabel("build-cop-override", LabelFunction.NO_BLOCK);
projectOperations
@@ -4445,8 +4487,8 @@
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
.setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4456,36 +4498,35 @@
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
voteLabel(changeId, "build-cop-override", 1);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(2);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.OVERRIDDEN, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.OVERRIDDEN, /* isLegacy= */ false);
assertSubmitRequirementStatus(
change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
}
@Test
- @Ignore("Test is flaky")
- public void submitRequirement_overriddenInChildProject() throws Exception {
+ public void submitRequirement_overriddenInChildProjectWithStricterRequirement() throws Exception {
configSubmitRequirement(
allProjects,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+1"))
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
.setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
.setAllowOverrideInChildProjects(true)
.build());
- // Override submit requirement in child project (requires code-review=+2 instead of +1)
+ // Override submit requirement in child project (requires Code-Review=+2 instead of +1)
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
.setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4495,31 +4536,102 @@
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
- voteLabel(changeId, "code-review", 1);
+ voteLabel(changeId, "Code-Review", 1);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
- voteLabel(changeId, "code-review", 2);
+ voteLabel(changeId, "Code-Review", 2);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
}
@Test
- @GerritConfig(
- name = "experiments.enabled",
- value = ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS)
+ public void submitRequirement_overriddenInChildProjectWithLessStrictRequirement()
+ throws Exception {
+ configSubmitRequirement(
+ allProjects,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
+ .setAllowOverrideInChildProjects(true)
+ .build());
+
+ // Override submit requirement in child project (requires Code-Review=+1 instead of +2)
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
+ .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ voteLabel(changeId, "Code-Review", 1);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
+ // +1 was enough to fulfill the requirement: override in child project applies
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+ // Legacy requirement that is coming from the label MaxWithBlock function. Still unsatisfied.
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
+ }
+
+ @Test
+ public void submitRequirement_overriddenInChildProjectAsDisabled() throws Exception {
+ configSubmitRequirement(
+ allProjects,
+ SubmitRequirement.builder()
+ .setName("Custom-Requirement")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
+ .setAllowOverrideInChildProjects(true)
+ .build());
+
+ // Override submit requirement in child project (requires Code-Review=+1 instead of +2)
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Custom-Requirement")
+ .setApplicabilityExpression(SubmitRequirementExpression.of("is:false"))
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("is:false"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
+ assertSubmitRequirementStatus(
+ change.submitRequirements,
+ "Custom-Requirement",
+ Status.NOT_APPLICABLE,
+ /* isLegacy= */ false);
+ }
+
+ @Test
public void submitRequirement_inheritedFromParentProject() throws Exception {
configSubmitRequirement(
allProjects,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+1"))
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
.setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4529,40 +4641,82 @@
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
- voteLabel(changeId, "code-review", 1);
+ voteLabel(changeId, "Code-Review", 1);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(2);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
// Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied.
assertSubmitRequirementStatus(
change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
}
@Test
- @GerritConfig(
- name = "experiments.enabled",
- value = ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS)
+ public void submitRequirement_overriddenSRInParentProjectIsInheritedByChildProject()
+ throws Exception {
+ // Define submit requirement in root project.
+ configSubmitRequirement(
+ allProjects,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
+ .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
+ .setAllowOverrideInChildProjects(true)
+ .build());
+
+ // Override submit requirement in parent project (requires Code-Review=+2 instead of +1).
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ Project.NameKey child = createProjectOverAPI("child", project, true, /* submitType= */ null);
+ TestRepository<InMemoryRepository> childRepo = cloneProject(child);
+ PushOneCommit.Result r = createChange(childRepo);
+ String changeId = r.getChangeId();
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ voteLabel(changeId, "Code-Review", 1);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ voteLabel(changeId, "Code-Review", 2);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+ }
+
+ @Test
public void submitRequirement_ignoredInChildProject_ifParentDoesNotAllowOverride()
throws Exception {
configSubmitRequirement(
allProjects,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+1"))
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
.setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
.setAllowOverrideInChildProjects(false)
.build());
- // Override submit requirement in child project (requires code-review=+2 instead of +1).
+ // Override submit requirement in child project (requires Code-Review=+2 instead of +1).
// Will have no effect since parent does not allow override.
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
.setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4572,14 +4726,320 @@
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
- voteLabel(changeId, "code-review", 1);
+ voteLabel(changeId, "Code-Review", 1);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(2);
// +1 was enough to fulfill the requirement: override in child project was ignored
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+ // Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied.
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
+ }
+
+ @Test
+ public void submitRequirement_ignoredInChildProject_ifParentAddsSRThatDoesNotAllowOverride()
+ throws Exception {
+ // Submit requirement in child project (requires Code-Review=+1)
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
+ .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ voteLabel(changeId, "Code-Review", 1);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
+ // +1 was enough to fulfill the requirement
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+ // Legacy requirement that is coming from the label MaxWithBlock function. Still unsatisfied.
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
+
+ // Add stricter non-overridable submit requirement in parent project (requires Code-Review=+2,
+ // instead of Code-Review=+1)
+ configSubmitRequirement(
+ allProjects,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ voteLabel(changeId, "Code-Review", 2);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+ }
+
+ @Test
+ public void submitRequirement_ignoredInChildProject_ifParentMakesSRNonOverridable()
+ throws Exception {
+ configSubmitRequirement(
+ allProjects,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
+ .setAllowOverrideInChildProjects(true)
+ .build());
+
+ // Override submit requirement in child project (requires Code-Review=+1 instead of +2)
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
+ .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ voteLabel(changeId, "Code-Review", 1);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
+ // +1 was enough to fulfill the requirement: override in child project applies
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+ // Legacy requirement that is coming from the label MaxWithBlock function. Still unsatisfied.
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
+
+ // Disallow overriding the submit requirement in the parent project.
+ configSubmitRequirement(
+ allProjects,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ voteLabel(changeId, "Code-Review", 2);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+ }
+
+ @Test
+ public void submitRequirement_ignoredInGrandChildProject_ifGrandParentDoesNotAllowOverride()
+ throws Exception {
+ configSubmitRequirement(
+ allProjects,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
+ .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ Project.NameKey grandChild =
+ createProjectOverAPI("grandChild", project, true, /* submitType= */ null);
+
+ // Override submit requirement in grand child project (requires Code-Review=+2 instead of +1).
+ // Will have no effect since grand parent does not allow override.
+ configSubmitRequirement(
+ grandChild,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ TestRepository<InMemoryRepository> grandChildRepo = cloneProject(grandChild);
+ PushOneCommit.Result r = createChange(grandChildRepo);
+ String changeId = r.getChangeId();
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ voteLabel(changeId, "Code-Review", 1);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
+ // +1 was enough to fulfill the requirement: override in grand child project was ignored
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+ // Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied.
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
+ }
+
+ @Test
+ public void submitRequirement_overrideOverideExpression() throws Exception {
+ // Define submit requirement in root project.
+ configSubmitRequirement(
+ allProjects,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
+ .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
+ .setAllowOverrideInChildProjects(true)
+ .build());
+
+ // Create Code-Review-Override label
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.function = "NoOp";
+ input.values = ImmutableMap.of("+1", "Override", " 0", "No Override");
+ gApi.projects().name(project.get()).label("Code-Review-Override").create(input).get();
+
+ // Allow to vote on the Code-Review-Override label.
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ TestProjectUpdate.allowLabel("Code-Review-Override")
+ .range(0, 1)
+ .ref("refs/*")
+ .group(REGISTERED_USERS)
+ .build())
+ .update();
+
+ // Override submit requirement in project (requires Code-Review-Override+1 as override instead
+ // of build-cop-override+1).
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
+ .setOverrideExpression(SubmitRequirementExpression.of("label:Code-Review-Override=+1"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ voteLabel(changeId, "Code-Review-Override", 1);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
+ // Code-Review-Override+1 was enough to fulfill the override expression of the requirement
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.OVERRIDDEN, /* isLegacy= */ false);
+ // Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied.
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
+ }
+
+ @Test
+ public void submitRequirement_partiallyOverriddenSRIsIgnored() throws Exception {
+ // Create build-cop-override label
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.function = "NoOp";
+ input.values = ImmutableMap.of("+1", "Override", " 0", "No Override");
+ gApi.projects().name(project.get()).label("build-cop-override").create(input).get();
+
+ // Allow to vote on the build-cop-override label.
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ TestProjectUpdate.allowLabel("build-cop-override")
+ .range(0, 1)
+ .ref("refs/*")
+ .group(REGISTERED_USERS)
+ .build())
+ .update();
+
+ // Define submit requirement in root project.
+ configSubmitRequirement(
+ allProjects,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
+ .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
+ .setAllowOverrideInChildProjects(true)
+ .build());
+
+ // Create Code-Review-Override label
+ gApi.projects().name(project.get()).label("Code-Review-Override").create(input).get();
+
+ // Allow to vote on the Code-Review-Override label.
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ TestProjectUpdate.allowLabel("Code-Review-Override")
+ .range(0, 1)
+ .ref("refs/*")
+ .group(REGISTERED_USERS)
+ .build())
+ .update();
+
+ // Override submit requirement in project (requires Code-Review-Override+1 as override instead
+ // of build-cop-override+1), but do not set all required properties (submittability expression
+ // is missing). We update the project.config file directly in the remote repository, since
+ // trying to push such a submit requirement would be rejected by the commit validation.
+ projectOperations
+ .project(project)
+ .forInvalidation()
+ .addProjectConfigUpdater(
+ config ->
+ config.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ "Code-Review",
+ ProjectConfig.KEY_SR_OVERRIDE_EXPRESSION,
+ "label:Code-Review-Override=+1"))
+ .invalidate();
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ voteLabel(changeId, "Code-Review-Override", 1);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ // The override expression in the project is satisfied, but it's ignored since the SR is
+ // incomplete.
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ voteLabel(changeId, "build-cop-override", 1);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
+ // The submit requirement is overridden now (the override expression in the child project is
+ // ignored)
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.OVERRIDDEN, /* isLegacy= */ false);
// Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied.
assertSubmitRequirementStatus(
change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
@@ -4588,11 +5048,9 @@
@Test
@GerritConfig(
name = "experiments.enabled",
- values = {
- ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS,
- ExperimentFeaturesConstants
- .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE
- })
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE)
public void submitRequirement_storedForClosedChanges() throws Exception {
for (SubmitType submitType : SubmitType.values()) {
Project.NameKey project = createProjectForPush(submitType);
@@ -4600,9 +5058,9 @@
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
+ .setName("Code-Review")
.setSubmittabilityExpression(
- SubmitRequirementExpression.create("label:code-review=+2"))
+ SubmitRequirementExpression.create("label:Code-Review=+2"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4610,12 +5068,12 @@
createChange(repo, "master", "Add a file", "foo", "content", "topic");
String changeId = r.getChangeId();
- voteLabel(changeId, "code-review", 2);
+ voteLabel(changeId, "Code-Review", 2);
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
revision.review(ReviewInput.approve());
@@ -4629,24 +5087,414 @@
assertThat(result.submittabilityExpressionResult().status())
.isEqualTo(SubmitRequirementExpressionResult.Status.PASS);
assertThat(result.submittabilityExpressionResult().expression().expressionString())
- .isEqualTo("label:code-review=+2");
+ .isEqualTo("label:Code-Review=+2");
}
}
@Test
@GerritConfig(
name = "experiments.enabled",
- values = {
- ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS,
- ExperimentFeaturesConstants
- .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE
- })
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE)
+ public void submitRequirement_storedForAbandonedChanges() throws Exception {
+ for (SubmitType submitType : SubmitType.values()) {
+ Project.NameKey project = createProjectForPush(submitType);
+ TestRepository<InMemoryRepository> repo = cloneProject(project);
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r =
+ createChange(repo, "master", "Add a file", "foo", "content", "topic");
+ String changeId = r.getChangeId();
+
+ voteLabel(changeId, "Code-Review", 2);
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+
+ gApi.changes().id(r.getChangeId()).abandon();
+ ChangeNotes notes = notesFactory.create(project, r.getChange().getId());
+ SubmitRequirementResult result =
+ notes.getSubmitRequirementsResult().stream().collect(MoreCollectors.onlyElement());
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
+ assertThat(result.submittabilityExpressionResult().status())
+ .isEqualTo(SubmitRequirementExpressionResult.Status.PASS);
+ assertThat(result.submittabilityExpressionResult().expression().expressionString())
+ .isEqualTo("label:Code-Review=+2");
+ }
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE)
+ public void submitRequirement_loadedFromTheLatestRevisionNoteForClosedChanges() throws Exception {
+ for (SubmitType submitType : SubmitType.values()) {
+ Project.NameKey project = createProjectForPush(submitType);
+ TestRepository<InMemoryRepository> repo = cloneProject(project);
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r =
+ createChange(repo, "master", "Add a file", "foo", "content", "topic");
+ String changeId = r.getChangeId();
+
+ // Abandon change. Submit requirements get stored in the revision note of patch-set 1.
+ gApi.changes().id(changeId).abandon();
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ // Restore the change.
+ gApi.changes().id(changeId).restore();
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ // Upload a second patch-set, fulfill the CR submit requirement.
+ amendChange(changeId, "refs/for/master", user, repo);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.revisions).hasSize(2);
+ voteLabel(changeId, "Code-Review", 2);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+
+ // Abandon the change.
+ gApi.changes().id(changeId).abandon();
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+ }
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE)
+ public void submitRequirement_abandonRestoreUpdateMerge() throws Exception {
+ for (SubmitType submitType : SubmitType.values()) {
+ Project.NameKey project = createProjectForPush(submitType);
+ TestRepository<InMemoryRepository> repo = cloneProject(project);
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r =
+ createChange(repo, "master", "Add a file", "foo", "content", "topic");
+ String changeId = r.getChangeId();
+
+ // Abandon change. Submit requirements get stored in the revision note of patch-set 1.
+ gApi.changes().id(changeId).abandon();
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ // Restore the change.
+ gApi.changes().id(changeId).restore();
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ // Update the change.
+ amendChange(changeId, "refs/for/master", user, repo);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.revisions).hasSize(2);
+ voteLabel(changeId, "Code-Review", 2);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+
+ // Merge the change.
+ gApi.changes().id(changeId).current().submit();
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+ }
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE)
+ public void submitRequirement_returnsEmpty_ForAbandonedChangeWithPreviouslyStoredSRs()
+ throws Exception {
+ for (SubmitType submitType : SubmitType.values()) {
+ Project.NameKey project = createProjectForPush(submitType);
+ TestRepository<InMemoryRepository> repo = cloneProject(project);
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r =
+ createChange(repo, "master", "Add a file", "foo", "content", "topic");
+ String changeId = r.getChangeId();
+
+ // Abandon change. Submit requirements get stored in the revision note of patch-set 1.
+ gApi.changes().id(changeId).abandon();
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ // Clear SRs for the project and update code-review label to be non-blocking.
+ clearSubmitRequirements(project);
+ LabelType cr =
+ TestLabels.codeReview().toBuilder().setFunction(LabelFunction.NO_BLOCK).build();
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().upsertLabelType(cr);
+ u.save();
+ }
+
+ // Restore the change. No SRs apply.
+ gApi.changes().id(changeId).restore();
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).isEmpty();
+
+ // Abandon the change. Still, no SRs apply.
+ gApi.changes().id(changeId).abandon();
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).isEmpty();
+ }
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE)
+ public void submitRequirement_returnsEmpty_ForMergedChangeWithPreviouslyStoredSRs()
+ throws Exception {
+ for (SubmitType submitType : SubmitType.values()) {
+ Project.NameKey project = createProjectForPush(submitType);
+ TestRepository<InMemoryRepository> repo = cloneProject(project);
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r =
+ createChange(repo, "master", "Add a file", "foo", "content", "topic");
+ String changeId = r.getChangeId();
+
+ // Abandon change. Submit requirements get stored in the revision note of patch-set 1.
+ gApi.changes().id(changeId).abandon();
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ // Clear SRs for the project and update code-review label to be non-blocking.
+ clearSubmitRequirements(project);
+ LabelType cr =
+ TestLabels.codeReview().toBuilder().setFunction(LabelFunction.NO_BLOCK).build();
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().upsertLabelType(cr);
+ u.save();
+ }
+
+ // Restore the change. No SRs apply.
+ gApi.changes().id(changeId).restore();
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).isEmpty();
+
+ // Merge the change. Still, no SRs apply.
+ gApi.changes().id(changeId).current().submit();
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).isEmpty();
+ }
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE)
+ public void submitRequirement_withMultipleAbandonAndRestore() throws Exception {
+ for (SubmitType submitType : SubmitType.values()) {
+ Project.NameKey project = createProjectForPush(submitType);
+ TestRepository<InMemoryRepository> repo = cloneProject(project);
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r =
+ createChange(repo, "master", "Add a file", "foo", "content", "topic");
+ String changeId = r.getChangeId();
+
+ // Abandon change. Submit requirements get stored in the revision note of patch-set 1.
+ gApi.changes().id(changeId).abandon();
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ // Restore the change.
+ gApi.changes().id(changeId).restore();
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ // Abandon the change again.
+ gApi.changes().id(changeId).abandon();
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ // Restore, vote CR=+2, and abandon again. Make sure the requirement is now satisfied.
+ gApi.changes().id(changeId).restore();
+ voteLabel(changeId, "Code-Review", 2);
+ gApi.changes().id(changeId).abandon();
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+ }
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE)
+ public void submitRequirement_retrievedFromNoteDbForAbandonedChanges() throws Exception {
+ for (SubmitType submitType : SubmitType.values()) {
+ Project.NameKey project = createProjectForPush(submitType);
+ TestRepository<InMemoryRepository> repo = cloneProject(project);
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r =
+ createChange(repo, "master", "Add a file", "foo", "content", "topic");
+ String changeId = r.getChangeId();
+ voteLabel(changeId, "Code-Review", 2);
+ gApi.changes().id(changeId).abandon();
+
+ // Add another submit requirement. This will not get returned for the abandoned change, since
+ // we return the state of the SR results when the change was abandoned.
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("New-Requirement")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("-has:unresolved"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+ ChangeInfo changeInfo =
+ gApi.changes().id(changeId).get(ListChangesOption.SUBMIT_REQUIREMENTS);
+ assertThat(changeInfo.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ changeInfo.submitRequirements,
+ "Code-Review",
+ Status.SATISFIED,
+ /* isLegacy= */ false,
+ /* submittabilityCondition= */ "label:Code-Review=+2");
+
+ // Restore the change, the new requirement will show up
+ gApi.changes().id(changeId).restore();
+ changeInfo = gApi.changes().id(changeId).get(ListChangesOption.SUBMIT_REQUIREMENTS);
+ assertThat(changeInfo.submitRequirements).hasSize(2);
+ assertSubmitRequirementStatus(
+ changeInfo.submitRequirements,
+ "Code-Review",
+ Status.SATISFIED,
+ /* isLegacy= */ false,
+ /* submittabilityCondition= */ "label:Code-Review=+2");
+ assertSubmitRequirementStatus(
+ changeInfo.submitRequirements,
+ "New-Requirement",
+ Status.SATISFIED,
+ /* isLegacy= */ false,
+ /* submittabilityCondition= */ "-has:unresolved");
+
+ // Abandon again, make sure the new requirement was persisted
+ gApi.changes().id(changeId).abandon();
+ changeInfo = gApi.changes().id(changeId).get(ListChangesOption.SUBMIT_REQUIREMENTS);
+ assertThat(changeInfo.submitRequirements).hasSize(2);
+ assertSubmitRequirementStatus(
+ changeInfo.submitRequirements,
+ "Code-Review",
+ Status.SATISFIED,
+ /* isLegacy= */ false,
+ /* submittabilityCondition= */ "label:Code-Review=+2");
+ assertSubmitRequirementStatus(
+ changeInfo.submitRequirements,
+ "New-Requirement",
+ Status.SATISFIED,
+ /* isLegacy= */ false,
+ /* submittabilityCondition= */ "-has:unresolved");
+ }
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE)
public void submitRequirement_retrievedFromNoteDbForClosedChanges() throws Exception {
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
.setAllowOverrideInChildProjects(false)
.build());
@@ -4656,14 +5504,14 @@
ChangeInfo change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
- voteLabel(changeId, "code-review", 2);
+ voteLabel(changeId, "Code-Review", 2);
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
gApi.changes().id(changeId).current().submit();
@@ -4671,26 +5519,24 @@
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("verified")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:verified=+1"))
+ .setName("Verified")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Verified=+1"))
.setAllowOverrideInChildProjects(false)
.build());
- // The new "verified" submit requirement is not returned, since this change is closed
+ // The new "Verified" submit requirement is not returned, since this change is closed
change = gApi.changes().id(changeId).get();
assertThat(change.submitRequirements).hasSize(1);
assertSubmitRequirementStatus(
- change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
}
@Test
@GerritConfig(
name = "experiments.enabled",
- values = {
- ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS,
- ExperimentFeaturesConstants
- .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE
- })
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE)
public void
submitRequirements_returnOneEntryForMatchingLegacyAndNonLegacyResultsWithTheSameName_ifLegacySubmitRecordsAreEnabled()
throws Exception {
@@ -4751,9 +5597,6 @@
}
@Test
- @GerritConfig(
- name = "experiments.enabled",
- value = ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS)
public void
submitRequirements_returnTwoEntriesForMismatchingLegacyAndNonLegacyResultsWithTheSameName_ifLegacySubmitRecordsAreEnabled()
throws Exception {
@@ -4807,9 +5650,38 @@
}
@Test
- @GerritConfig(
- name = "experiments.enabled",
- value = ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS)
+ public void submitRequirements_skippedIfLegacySRIsBasedOnOptionalLabel() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ SubmitRule r1 =
+ createSubmitRule("r1", SubmitRecord.Status.OK, "CR", SubmitRecord.Label.Status.MAY);
+ try (Registration registration = extensionRegistry.newRegistration().add(r1)) {
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ Collection<SubmitRequirementResultInfo> submitRequirements = change.submitRequirements;
+ assertThat(submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
+ }
+ }
+
+ @Test
+ public void submitRequirement_notSkippedIfLegacySRIsBasedOnNonOptionalLabel() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ SubmitRule r1 =
+ createSubmitRule("r1", SubmitRecord.Status.OK, "CR", SubmitRecord.Label.Status.OK);
+ try (Registration registration = extensionRegistry.newRegistration().add(r1)) {
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ Collection<SubmitRequirementResultInfo> submitRequirements = change.submitRequirements;
+ assertThat(submitRequirements).hasSize(2);
+ assertSubmitRequirementStatus(
+ submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
+ assertSubmitRequirementStatus(
+ submitRequirements, "CR", Status.SATISFIED, /* isLegacy= */ true);
+ }
+ }
+
+ @Test
public void submitRequirements_returnForLegacySubmitRecords_ifEnabled() throws Exception {
configLabel("build-cop-override", LabelFunction.MAX_WITH_BLOCK);
projectOperations
@@ -4862,22 +5734,19 @@
}
@Test
- @GerritConfig(
- name = "experiments.enabled",
- value = ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS)
public void submitRequirement_backFilledFromIndexForActiveChanges() throws Exception {
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
.setAllowOverrideInChildProjects(false)
.build());
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
- voteLabel(changeId, "code-review", 2);
+ voteLabel(changeId, "Code-Review", 2);
// Query the change. ChangeInfo is back-filled from the change index.
List<ChangeInfo> changeInfos =
@@ -4889,7 +5758,7 @@
assertThat(changeInfos).hasSize(1);
assertSubmitRequirementStatus(
changeInfos.get(0).submitRequirements,
- "code-review",
+ "Code-Review",
Status.SATISFIED,
/* isLegacy= */ false);
}
@@ -4897,68 +5766,647 @@
@Test
@GerritConfig(
name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE)
+ public void submitRequirement_backFilledFromIndexForClosedChanges() throws Exception {
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ voteLabel(changeId, "Code-Review", 2);
+ gApi.changes().id(changeId).current().submit();
+
+ // Query the change. ChangeInfo is back-filled from the change index.
+ List<ChangeInfo> changeInfos =
+ gApi.changes()
+ .query()
+ .withQuery("project:{" + project.get() + "} (status:open OR status:closed)")
+ .withOptions(ImmutableSet.of(ListChangesOption.SUBMIT_REQUIREMENTS))
+ .get();
+ assertThat(changeInfos).hasSize(1);
+ assertSubmitRequirementStatus(
+ changeInfos.get(0).submitRequirements,
+ "Code-Review",
+ Status.SATISFIED,
+ /* isLegacy= */ false);
+ }
+
+ @Test
+ public void submitRequirement_applicabilityExpressionIsAlwaysHidden() throws Exception {
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setApplicabilityExpression(SubmitRequirementExpression.of("branch:refs/heads/master"))
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ voteLabel(changeId, "Code-Review", 2);
+ ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+ SubmitRequirementResultInfo requirement =
+ changeInfo.submitRequirements.stream().collect(MoreCollectors.onlyElement());
+ assertSubmitRequirementExpression(
+ requirement.applicabilityExpressionResult,
+ /* expression= */ null,
+ /* passingAtoms= */ null,
+ /* failingAtoms= */ null,
+ /* fulfilled= */ true);
+ }
+
+ @Test
+ public void submitRequirements_eliminatesDuplicatesForLegacyNonMatchingSRs() throws Exception {
+ // If a custom/prolog submit rule emits the same label name multiple times, we merge these into
+ // a single submit requirement result: in this test, we have two different submit rules that
+ // return the same label name, one as "OK" and the other as "NEED". The submit requirements
+ // API favours the blocking entry and returns one SR result with status=UNSATISFIED.
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ SubmitRule r1 =
+ createSubmitRule("r1", SubmitRecord.Status.OK, "CR", SubmitRecord.Label.Status.OK);
+ SubmitRule r2 =
+ createSubmitRule("r2", SubmitRecord.Status.NOT_READY, "CR", SubmitRecord.Label.Status.NEED);
+ try (Registration registration = extensionRegistry.newRegistration().add(r1).add(r2)) {
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ Collection<SubmitRequirementResultInfo> submitRequirements = change.submitRequirements;
+ assertThat(submitRequirements).hasSize(2);
+ assertSubmitRequirementStatus(
+ submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
+ assertSubmitRequirementStatus(
+ submitRequirements, "CR", Status.UNSATISFIED, /* isLegacy= */ true);
+ }
+ }
+
+ @Test
+ public void submitRequirements_eliminatesDuplicatesForLegacyMatchingSRs() throws Exception {
+ // If a custom/prolog submit rule emits the same label name multiple times, we merge these into
+ // a single submit requirement result: in this test, we have two different submit rules that
+ // return the same label name, but both are fulfilled (i.e. they both allow submission). The
+ // submit requirements API returns one SR result with status=SATISFIED.
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ SubmitRule r1 =
+ createSubmitRule("r1", SubmitRecord.Status.OK, "CR", SubmitRecord.Label.Status.OK);
+ SubmitRule r2 =
+ createSubmitRule("r2", SubmitRecord.Status.OK, "CR", SubmitRecord.Label.Status.MAY);
+ try (Registration registration = extensionRegistry.newRegistration().add(r1).add(r2)) {
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ Collection<SubmitRequirementResultInfo> submitRequirements = change.submitRequirements;
+ assertThat(submitRequirements).hasSize(2);
+ assertSubmitRequirementStatus(
+ submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
+ assertSubmitRequirementStatus(
+ submitRequirements, "CR", Status.SATISFIED, /* isLegacy= */ true);
+ }
+ }
+
+ @Test
+ public void submitRequirements_eliminatesMultipleDuplicatesForLegacyMatchingSRs()
+ throws Exception {
+ // If a custom/prolog submit rule emits the same label name multiple times, we merge these into
+ // a single submit requirement result: in this test, we have five different submit rules that
+ // return the same label name, all with an "OK" status. The submit requirements API returns
+ // a single SR result with status=SATISFIED.
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ try (Registration registration = extensionRegistry.newRegistration()) {
+ IntStream.range(0, 5)
+ .forEach(
+ i ->
+ registration.add(
+ createSubmitRule(
+ "r" + i, SubmitRecord.Status.OK, "CR", SubmitRecord.Label.Status.OK)));
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ Collection<SubmitRequirementResultInfo> submitRequirements = change.submitRequirements;
+ assertThat(submitRequirements).hasSize(2);
+ assertSubmitRequirementStatus(
+ submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
+ assertSubmitRequirementStatus(
+ submitRequirements, "CR", Status.SATISFIED, /* isLegacy= */ true);
+ }
+ }
+
+ @Test
+ public void submitRequirement_duplicateSubmitRequirement_sameCase() throws Exception {
+ // Define 2 submit requirements with exact same name but different submittability expression.
+ try (TestRepository<Repository> repo =
+ new TestRepository<>(repoManager.openRepository(project))) {
+ Ref ref = repo.getRepository().exactRef(RefNames.REFS_CONFIG);
+ RevCommit head = repo.getRevWalk().parseCommit(ref.getObjectId());
+ RevObject blob = repo.get(head.getTree(), ProjectConfig.PROJECT_CONFIG);
+ byte[] data = repo.getRepository().open(blob).getCachedBytes(Integer.MAX_VALUE);
+ String projectConfig = RawParseUtils.decode(data);
+
+ repo.update(
+ RefNames.REFS_CONFIG,
+ repo.commit()
+ .parent(head)
+ .message("Set project config")
+ .add(
+ ProjectConfig.PROJECT_CONFIG,
+ projectConfig
+ // JGit parses this as a list value:
+ // submit-requirement.Code-Review.submittableIf =
+ // [label:Code-Review=+2, label:Code-Review=+1]
+ // if getString is used to read submittableIf JGit returns the last value
+ // (label:Code-Review=+1)
+ + "[submit-requirement \"Code-Review\"]\n"
+ + " submittableIf = label:Code-Review=+2\n"
+ + "[submit-requirement \"Code-Review\"]\n"
+ + " submittableIf = label:Code-Review=+1\n"));
+ }
+ projectCache.evict(project);
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ voteLabel(changeId, "Code-Review", 1);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
+ // The submit requirement is fulfilled now, since label:Code-Review=+1 applies as submittability
+ // expression (see comment above)
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+ // Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied.
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
+ }
+
+ @Test
+ public void submitRequirement_duplicateSubmitRequirement_differentCase() throws Exception {
+ // Define 2 submit requirements with same name but different case and different submittability
+ // expression.
+ try (TestRepository<Repository> repo =
+ new TestRepository<>(repoManager.openRepository(project))) {
+ Ref ref = repo.getRepository().exactRef(RefNames.REFS_CONFIG);
+ RevCommit head = repo.getRevWalk().parseCommit(ref.getObjectId());
+ RevObject blob = repo.get(head.getTree(), ProjectConfig.PROJECT_CONFIG);
+ byte[] data = repo.getRepository().open(blob).getCachedBytes(Integer.MAX_VALUE);
+ String projectConfig = RawParseUtils.decode(data);
+
+ repo.update(
+ RefNames.REFS_CONFIG,
+ repo.commit()
+ .parent(head)
+ .message("Set project config")
+ .add(
+ ProjectConfig.PROJECT_CONFIG,
+ projectConfig
+ // ProjectConfig processes the submit requirements in the order in which they
+ // appear (1. Code-Review, 2. code-review) and ignores any further submit
+ // requirement if its name case-insensitively matches the name of a submit
+ // requirement that has already been seen. This means the Code-Review submit
+ // requirement applies and the code-review submit requirement is ignored.
+ + "[submit-requirement \"Code-Review\"]\n"
+ + " submittableIf = label:Code-Review=+2\n"
+ + "[submit-requirement \"code-review\"]\n"
+ + " submittableIf = label:Code-Review=+1\n"));
+ }
+ projectCache.evict(project);
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ voteLabel(changeId, "Code-Review", 1);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ // Still not satisfied since the Code-Review submit requirement with label:Code-Review=+2 as
+ // submittability expression applies (see comment above).
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ voteLabel(changeId, "Code-Review", 2);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ // The submit requirement is fulfilled now, since label:Code-Review=+2 applies as submittability
+ // expression (see comment above)
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+ }
+
+ @Test
+ public void submitRequirement_overrideInheritedSRWithDifferentNameCasing() throws Exception {
+ // Define submit requirement in root project and allow override.
+ configSubmitRequirement(
+ allProjects,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
+ .setAllowOverrideInChildProjects(true)
+ .build());
+
+ // Define a submit requirement with the same name in the child project that differs by case and
+ // has a different submittability expression (requires Code-Review=+1 instead of +2).
+ // This overrides the inherited submit requirement with the same name, although the case is
+ // different.
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("code-review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
+ .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ voteLabel(changeId, "Code-Review", 1);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
+ // +1 was enough to fulfill the requirement since the override applies
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
+ // Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied.
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
+ }
+
+ @Test
+ public void submitRequirement_cannotOverrideNonOverridableInheritedSRWithDifferentNameCasing()
+ throws Exception {
+ // Define submit requirement in root project and disallow override.
+ configSubmitRequirement(
+ allProjects,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ // Define a submit requirement with the same name in the child project that differs by case and
+ // has a different submittability expression (requires Code-Review=+1 instead of +2).
+ // This is ignored since the inherited submit requirement with the same name (different case)
+ // disallows overriding.
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("code-review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
+ .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ voteLabel(changeId, "Code-Review", 1);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ // Still not satisfied since the override is ignored.
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ voteLabel(changeId, "Code-Review", 2);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE)
+ public void globalSubmitRequirement_storedForClosedChanges() throws Exception {
+ SubmitRequirement globalSubmitRequirement =
+ SubmitRequirement.builder()
+ .setName("global-submit-requirement")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("topic:test"))
+ .setAllowOverrideInChildProjects(false)
+ .build();
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(globalSubmitRequirement)) {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
+ assertSubmitRequirementStatus(
+ change.submitRequirements,
+ "global-submit-requirement",
+ Status.UNSATISFIED,
+ /* isLegacy= */ false);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
+
+ voteLabel(changeId, "Code-Review", 2);
+ gApi.changes().id(changeId).topic("test");
+
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
+ assertSubmitRequirementStatus(
+ change.submitRequirements,
+ "global-submit-requirement",
+ Status.SATISFIED,
+ /* isLegacy= */ false);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
+
+ gApi.changes().id(changeId).current().submit();
+
+ ChangeNotes notes = notesFactory.create(project, r.getChange().getId());
+ SubmitRequirementResult result =
+ notes.getSubmitRequirementsResult().stream().collect(MoreCollectors.onlyElement());
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
+ assertThat(result.submittabilityExpressionResult().status())
+ .isEqualTo(SubmitRequirementExpressionResult.Status.PASS);
+ assertThat(result.submittabilityExpressionResult().expression().expressionString())
+ .isEqualTo("topic:test");
+ }
+ }
+
+ @Test
+ public void projectSubmitRequirementDuplicatesGlobal_overrideNotAllowed_globalEvaluated()
+ throws Exception {
+ SubmitRequirement globalSubmitRequirement =
+ SubmitRequirement.builder()
+ .setName("CoDe-reView")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("topic:test"))
+ .setAllowOverrideInChildProjects(false)
+ .build();
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(globalSubmitRequirement)) {
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ // Vote does not satisfy submit requirement, because the global definition is evaluated.
+ voteLabel(changeId, "CoDe-reView", 2);
+
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "CoDe-reView", Status.UNSATISFIED, /* isLegacy= */ false);
+ // In addition, the legacy submit requirement is emitted, since the status mismatch
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
+
+ // Setting the topic satisfies the global definition.
+ gApi.changes().id(changeId).topic("test");
+
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "CoDe-reView", Status.SATISFIED, /* isLegacy= */ false);
+ }
+ }
+
+ @Test
+ public void projectSubmitRequirementDuplicatesGlobal_overrideAllowed_projectRequirementEvaluated()
+ throws Exception {
+ SubmitRequirement globalSubmitRequirement =
+ SubmitRequirement.builder()
+ .setName("CoDe-reView")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("topic:test"))
+ .setAllowOverrideInChildProjects(true)
+ .build();
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(globalSubmitRequirement)) {
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Code-Review")
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ // Setting the topic does not satisfy submit requirement, because the project definition is
+ // evaluated.
+ gApi.changes().id(changeId).topic("test");
+
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ // There is no mismatch with legacy submit requirement, so the single result is emitted.
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ // Voting satisfies the project definition.
+ voteLabel(changeId, "Code-Review", 2);
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
+ }
+ }
+
+ @Test
+ public void legacySubmitRequirementDuplicatesGlobal_statusMatches_globalReturned()
+ throws Exception {
+ // The behaviour does not depend on AllowOverrideInChildProject in global submit requirement.
+ testLegacySubmitRequirementDuplicatesGlobalStatusMatches(/*allowOverrideInChildProject=*/ true);
+ testLegacySubmitRequirementDuplicatesGlobalStatusMatches(
+ /*allowOverrideInChildProject=*/ false);
+ }
+
+ private void testLegacySubmitRequirementDuplicatesGlobalStatusMatches(
+ boolean allowOverrideInChildProject) throws Exception {
+ SubmitRequirement globalSubmitRequirement =
+ SubmitRequirement.builder()
+ .setName("CoDe-reView")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("topic:test"))
+ .setAllowOverrideInChildProjects(allowOverrideInChildProject)
+ .build();
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(globalSubmitRequirement)) {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ // Both are evaluated, but only the global is returned, since both are unsatisfied
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "CoDe-reView", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ // Both are evaluated, but only the global is returned, since both are satisfied
+ voteLabel(changeId, "Code-Review", 2);
+ gApi.changes().id(changeId).topic("test");
+
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "CoDe-reView", Status.SATISFIED, /* isLegacy= */ false);
+ }
+ }
+
+ @Test
+ public void legacySubmitRequirementDuplicatesGlobal_statusDoesNotMatch_bothRecordsReturned()
+ throws Exception {
+ // The behaviour does not depend on AllowOverrideInChildProject in global submit requirement.
+ testLegacySubmitRequirementDuplicatesGlobalStatusDoesNotMatch(
+ /*allowOverrideInChildProject=*/ true);
+ testLegacySubmitRequirementDuplicatesGlobalStatusDoesNotMatch(
+ /*allowOverrideInChildProject=*/ false);
+ }
+
+ private void testLegacySubmitRequirementDuplicatesGlobalStatusDoesNotMatch(
+ boolean allowOverrideInChildProject) throws Exception {
+ SubmitRequirement globalSubmitRequirement =
+ SubmitRequirement.builder()
+ .setName("CoDe-reView")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("topic:test"))
+ .setAllowOverrideInChildProjects(allowOverrideInChildProject)
+ .build();
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(globalSubmitRequirement)) {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ // Both are evaluated, but only the global is returned, since both are unsatisfied
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(1);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "CoDe-reView", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ // Both are evaluated and both are returned, since result mismatch
+ voteLabel(changeId, "Code-Review", 2);
+
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "CoDe-reView", Status.UNSATISFIED, /* isLegacy= */ false);
+
+ gApi.changes().id(changeId).topic("test");
+ gApi.changes().id(changeId).reviewer(admin.id().toString()).deleteVote(LabelId.CODE_REVIEW);
+
+ change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
+ assertThat(change.submitRequirements).hasSize(2);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "CoDe-reView", Status.SATISFIED, /* isLegacy= */ false);
+ }
+ }
+
+ @Test
+ public void submitRequirements_disallowsTheIsSubmittableOperator() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("Wrong-Req")
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("is:submittable"))
+ .setAllowOverrideInChildProjects(false)
+ .build());
+
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ SubmitRequirementResultInfo srResult =
+ change.submitRequirements.stream()
+ .filter(sr -> sr.name.equals("Wrong-Req"))
+ .collect(MoreCollectors.onlyElement());
+ assertThat(srResult.status).isEqualTo(Status.ERROR);
+ assertThat(srResult.submittabilityExpressionResult.errorMessage)
+ .isEqualTo("Operator 'is:submittable' cannot be used in submit requirement expressions.");
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
values = {
- ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS,
ExperimentFeaturesConstants
.GERRIT_BACKEND_REQUEST_FEATURE_STORE_SUBMIT_REQUIREMENTS_ON_MERGE
})
- public void submitRequirement_backFilledFromIndexForClosedChanges() throws Exception {
+ public void submitRequirements_forcedByDirectSubmission() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(REGISTERED_USERS))
+ .update();
+
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setName("My-Requirement")
+ // Submit requirement is always unsatisfied, but we are going to bypass it.
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("is:false"))
.setAllowOverrideInChildProjects(false)
.build());
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
+ pushFactory.create(admin.newIdent(), testRepo, changeId).to("refs/for/master%submit");
- voteLabel(changeId, "code-review", 2);
- gApi.changes().id(changeId).current().submit();
-
- // Query the change. ChangeInfo is back-filled from the change index.
- List<ChangeInfo> changeInfos =
- gApi.changes()
- .query()
- .withQuery("project:{" + project.get() + "} (status:open OR status:closed)")
- .withOptions(ImmutableSet.of(ListChangesOption.SUBMIT_REQUIREMENTS))
- .get();
- assertThat(changeInfos).hasSize(1);
+ ChangeInfo change = gApi.changes().id(changeId).get();
+ assertThat(change.submitRequirements).hasSize(2);
assertSubmitRequirementStatus(
- changeInfos.get(0).submitRequirements,
- "code-review",
- Status.SATISFIED,
- /* isLegacy= */ false);
+ change.submitRequirements, "My-Requirement", Status.FORCED, /* isLegacy= */ false);
+ assertSubmitRequirementStatus(
+ change.submitRequirements, "Code-Review", Status.FORCED, /* isLegacy= */ true);
}
@Test
- public void submitRequirements_notServedIfExperimentNotEnabled() throws Exception {
+ public void submitRequirement_evaluatedWithInternalUserCredentials() throws Exception {
+ GroupInput in = new GroupInput();
+ in.name = "invisible-group";
+ in.visibleToAll = false;
+ in.ownerId = adminGroupUuid().get();
+ gApi.groups().create(in);
+
configSubmitRequirement(
project,
SubmitRequirement.builder()
- .setName("code-review")
- .setSubmittabilityExpression(SubmitRequirementExpression.create("label:code-review=+2"))
+ .setName("My-Requirement")
+ .setApplicabilityExpression(SubmitRequirementExpression.of("ownerin:invisible-group"))
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("is:true"))
.setAllowOverrideInChildProjects(false)
.build());
+ requestScopeOperations.setApiUser(user.id());
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
ChangeInfo change = gApi.changes().id(changeId).get();
- assertThat(change.submitRequirements).isEmpty();
-
- voteLabel(changeId, "code-review", -1);
- change = gApi.changes().id(changeId).get();
- assertThat(change.submitRequirements).isEmpty();
-
- voteLabel(changeId, "code-review", 2);
- change = gApi.changes().id(changeId).get();
- assertThat(change.submitRequirements).isEmpty();
-
- gApi.changes().id(changeId).current().submit();
- change = gApi.changes().id(changeId).get();
- assertThat(change.submitRequirements).isEmpty();
+ SubmitRequirementResultInfo srResult =
+ change.submitRequirements.stream()
+ .filter(sr -> sr.name.equals("My-Requirement"))
+ .collect(MoreCollectors.onlyElement());
+ assertThat(srResult.status).isEqualTo(Status.NOT_APPLICABLE);
}
@Test
@@ -5213,7 +6661,7 @@
private void setChangeStatus(Change.Id id, Change.Status newStatus) throws Exception {
try (BatchUpdate batchUpdate =
- batchUpdateFactory.create(project, atrScope.get().getUser(), TimeUtil.nowTs())) {
+ batchUpdateFactory.create(project, atrScope.get().getUser(), TimeUtil.now())) {
batchUpdate.addOp(id, new ChangeStatusUpdateOp(newStatus));
batchUpdate.execute();
}
@@ -5545,10 +6993,30 @@
requirementName,
status,
results.stream()
- .map(r -> String.format("%s=%s", r.name, r.status))
+ .map(r -> String.format("%s=%s, legacy=%s", r.name, r.status, r.isLegacy))
.collect(toImmutableList())));
}
+ private void assertSubmitRequirementExpression(
+ SubmitRequirementExpressionInfo result,
+ @Nullable String expression,
+ @Nullable List<String> passingAtoms,
+ @Nullable List<String> failingAtoms,
+ boolean fulfilled) {
+ assertThat(result.expression).isEqualTo(expression);
+ if (passingAtoms == null) {
+ assertThat(result.passingAtoms).isNull();
+ } else {
+ assertThat(result.passingAtoms).containsExactlyElementsIn(passingAtoms);
+ }
+ if (failingAtoms == null) {
+ assertThat(result.failingAtoms).isNull();
+ } else {
+ assertThat(result.failingAtoms).containsExactlyElementsIn(failingAtoms);
+ }
+ assertThat(result.fulfilled).isEqualTo(fulfilled);
+ }
+
private Project.NameKey createProjectForPush(SubmitType submitType) throws Exception {
Project.NameKey project = projectOperations.newProject().submitType(submitType).create();
projectOperations
@@ -5560,6 +7028,22 @@
return project;
}
+ private static SubmitRule createSubmitRule(
+ String ruleName,
+ SubmitRecord.Status srStatus,
+ String labelName,
+ SubmitRecord.Label.Status labelStatus) {
+ return changeData -> {
+ SubmitRecord r = new SubmitRecord();
+ r.ruleName = ruleName;
+ r.status = srStatus;
+ SubmitRecord.Label label = new SubmitRecord.Label();
+ label.label = labelName;
+ label.status = labelStatus;
+ r.labels = Arrays.asList(label);
+ return Optional.of(r);
+ };
+ }
/** Returns a hard-coded submit record containing all fields. */
private static class TestSubmitRule implements SubmitRule {
@Override
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeSubmitRequirementIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeSubmitRequirementIT.java
index 96db71a..c6b57da 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeSubmitRequirementIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeSubmitRequirementIT.java
@@ -19,7 +19,6 @@
import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.entities.LegacySubmitRequirement;
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.extensions.annotations.Exports;
@@ -29,7 +28,6 @@
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.LegacySubmitRequirementInfo;
import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.rules.SubmitRule;
import com.google.inject.Inject;
@@ -203,37 +201,6 @@
assertThat(rule.numberOfEvaluations.get()).isEqualTo(0);
}
- @Test
- @GerritConfig(
- name = "experiments.enabled",
- values = {
- ExperimentFeaturesConstants
- .GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS_BACKFILLING_ON_DASHBOARD,
- ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_ENABLE_SUBMIT_REQUIREMENTS
- })
- public void submitRuleIsInvokedWhenQueryingChangeWithExperiment() throws Exception {
- PushOneCommit.Result r = createChange("Some Change", "foo.txt", "some content");
- String changeId = r.getChangeId();
-
- rule.numberOfEvaluations.set(0);
- gApi.changes().query(changeId).withOptions(ListChangesOption.SUBMIT_REQUIREMENTS).get();
-
- // Submit rules are invoked
- assertThat(rule.numberOfEvaluations.get()).isEqualTo(1);
- }
-
- @Test
- public void submitRuleIsNotInvokedWhenQueryingChangeWithoutExperiment() throws Exception {
- PushOneCommit.Result r = createChange("Some Change", "foo.txt", "some content");
- String changeId = r.getChangeId();
-
- rule.numberOfEvaluations.set(0);
- gApi.changes().query(changeId).withOptions(ListChangesOption.SUBMIT_REQUIREMENTS).get();
-
- // Submit rules are not invoked
- assertThat(rule.numberOfEvaluations.get()).isEqualTo(0);
- }
-
private List<ChangeInfo> queryIsSubmittable() throws Exception {
return gApi.changes().query("is:submittable project:" + project.get()).get();
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
index 96bc65d..f8991b4 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
@@ -117,6 +117,7 @@
COMMENT_TEXT.length());
@Captor private ArgumentCaptor<ImmutableList<CommentForValidation>> captor;
+ @Captor private ArgumentCaptor<CommentValidationContext> captorCtx;
private static final Correspondence<CommentForValidation, CommentForValidation>
COMMENT_CORRESPONDENCE =
@@ -152,7 +153,7 @@
@Test
public void validateCommentsInInput_commentOK() throws Exception {
PushOneCommit.Result r = createChange();
- when(mockCommentValidator.validateComments(eq(contextFor(r)), captor.capture()))
+ when(mockCommentValidator.validateComments(captorCtx.capture(), captor.capture()))
.thenReturn(ImmutableList.of());
ReviewInput input = new ReviewInput().message(COMMENT_TEXT);
@@ -165,6 +166,7 @@
assertValidatorCalledWith(CHANGE_MESSAGE_FOR_VALIDATION, FILE_COMMENT_FOR_VALIDATION);
assertThat(testCommentHelper.getPublishedComments(r.getChangeId())).hasSize(1);
+ assertThat(captorCtx.getAllValues()).containsExactly(contextFor(r));
}
@Test
@@ -985,7 +987,9 @@
private static CommentValidationContext contextFor(PushOneCommit.Result result) {
return CommentValidationContext.create(
- result.getChange().getId().get(), result.getChange().project().get());
+ result.getChange().getId().get(),
+ result.getChange().project().get(),
+ result.getChange().change().getDest().branch());
}
private void assertValidatorCalledWith(CommentForValidation... commentsForValidation) {
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
index 97b7148..267f5a7 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
@@ -251,7 +251,7 @@
private void markMergedChangePrivate(Change.Id changeId) throws Exception {
try (BatchUpdate u =
batchUpdateFactory.create(
- project, identifiedUserFactory.create(admin.id()), TimeUtil.nowTs())) {
+ project, identifiedUserFactory.create(admin.id()), TimeUtil.now())) {
u.addOp(
changeId,
new BatchUpdateOp() {
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
index ff88f31..0cdb9b4 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
@@ -80,7 +80,9 @@
assertThat(thrown)
.hasMessageThat()
.contains("Failed to submit 1 change due to the following problems");
- assertThat(thrown).hasMessageThat().contains("needs Is-Pure-Revert");
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("submit requirement 'Is-Pure-Revert' is unsatisfied.");
}
@Test
@@ -101,7 +103,9 @@
assertThat(thrown)
.hasMessageThat()
.contains("Failed to submit 1 change due to the following problems");
- assertThat(thrown).hasMessageThat().contains("needs Is-Pure-Revert");
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("submit requirement 'Is-Pure-Revert' is unsatisfied.");
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
index 3888679..ff8a2d0 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -34,7 +34,10 @@
import com.google.common.cache.Cache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
+import com.google.common.collect.MoreCollectors;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
@@ -44,12 +47,15 @@
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.client.ChangeKind;
@@ -58,13 +64,18 @@
import com.google.gerrit.extensions.common.FileInfo;
import com.google.gerrit.server.change.ChangeKindCacheImpl;
import com.google.gerrit.server.project.testing.TestLabels;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.rules.SubmitRule;
import com.google.inject.Inject;
import com.google.inject.name.Named;
+import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
+import java.util.stream.Collectors;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.Before;
import org.junit.Test;
@@ -75,6 +86,7 @@
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private ChangeOperations changeOperations;
@Inject private ChangeKindCreator changeKindCreator;
+ @Inject private ExtensionRegistry extensionRegistry;
@Inject
@Named("change_kind")
@@ -260,6 +272,113 @@
}
@Test
+ public void sticky_copiedToLatestPatchSetFromSubmitRecords() throws Exception {
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().updateLabelType(LabelId.VERIFIED, b -> b.setFunction(LabelFunction.NO_BLOCK));
+ u.save();
+ }
+
+ // This test is covering the backfilling logic for changes which have been submitted, based on
+ // copied approvals, before Gerrit persisted copied votes as Copied-Label footers in NoteDb. It
+ // verifies that for such changes copied approvals are returned from the API even if the copied
+ // votes were not persisted as Copied-Label footers.
+ //
+ // In other words, this test verifies that given a change that was approved by a copied vote and
+ // then submitted and for which the copied approval is not persisted as a Copied-Label footer in
+ // NoteDb the copied approval is backfilled from the corresponding Submitted-With footer that
+ // got written to NoteDb on submit.
+ //
+ // Creating such a change would be possible by running the old Gerrit code from before Gerrit
+ // persisted copied labels as Copied-Label footers. However since this old Gerrit code is no
+ // longer available, the test needs to apply a trick to create a change in this state. It
+ // configures a fake submit rule, that pretends that an approval for a non-sticky label from an
+ // old patch set is still present on the current patch set and allows to submit the change.
+ // Since the label is non-sticky no Copied-Label footer is written for it. On submit the fake
+ // submit rule results in a Submitted-With footer that records the label as approved (although
+ // the label is actually not present on the current patch set). This is exactly the change state
+ // that we would have had by running the old code if submit was based on a copied label. As
+ // result of the backfilling logic we expect that this "copied" label (the label that is
+ // mentioned in the Submitted-With footer) is returned from the API.
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(new TestSubmitRule(user.id()))) {
+ // We want to add a vote on PS1, then not copy it to PS2, but include it in submit records
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ // Vote on patch-set 1
+ vote(admin, changeId, 2, 1);
+ vote(user, changeId, 1, -1);
+
+ // Upload patch-set 2. Change user's "Verified" vote on PS2.
+ changeOperations
+ .change(Change.id(r.getChange().getId().get()))
+ .newPatchset()
+ .file("new_file")
+ .content("content")
+ .commitMessage("Upload PS2")
+ .create();
+ vote(admin, changeId, 2, 1);
+ vote(user, changeId, 1, 1);
+
+ // Upload patch-set 3
+ changeOperations
+ .change(Change.id(r.getChange().getId().get()))
+ .newPatchset()
+ .file("another_file")
+ .content("content")
+ .commitMessage("Upload PS3")
+ .create();
+ vote(admin, changeId, 2, 1);
+
+ List<PatchSetApproval> patchSetApprovals =
+ notesFactory.create(project, r.getChange().getId()).getApprovalsWithCopied().values()
+ .stream()
+ .sorted(comparing(a -> a.patchSetId().get()))
+ .collect(toImmutableList());
+
+ // There's no verified approval on PS#3.
+ assertThat(
+ patchSetApprovals.stream()
+ .filter(
+ a ->
+ a.accountId().equals(user.id())
+ && a.label().equals(TestLabels.verified().getName())
+ && a.patchSetId().get() == 3)
+ .collect(Collectors.toList()))
+ .isEmpty();
+
+ // Submit the change. The TestSubmitRule will store a "submit record" containing a label
+ // voted by user, but the latest patch-set does not have an approval for this user, hence
+ // it will be copied if we request approvals after the change is merged.
+ requestScopeOperations.setApiUser(admin.id());
+ gApi.changes().id(changeId).current().submit();
+
+ patchSetApprovals =
+ notesFactory.create(project, r.getChange().getId()).getApprovalsWithCopied().values()
+ .stream()
+ .sorted(comparing(a -> a.patchSetId().get()))
+ .collect(toImmutableList());
+
+ // Get the copied approval for user on PS3 for the "Verified" label.
+ PatchSetApproval verifiedApproval =
+ patchSetApprovals.stream()
+ .filter(
+ a ->
+ a.accountId().equals(user.id())
+ && a.label().equals(TestLabels.verified().getName())
+ && a.patchSetId().get() == 3)
+ .collect(MoreCollectors.onlyElement());
+
+ assertCopied(
+ verifiedApproval,
+ /* psId= */ 3,
+ TestLabels.verified().getName(),
+ (short) 1,
+ /* copied= */ true);
+ }
+ }
+
+ @Test
public void stickyOnCopyValues() throws Exception {
TestAccount user2 = accountCreator.user2();
@@ -1265,6 +1384,35 @@
assertThat(nonCopiedSecondPatchsetRemovedVote.copied()).isFalse();
}
+ @Test
+ public void reviewerStickyVotingCanBeRemoved() throws Exception {
+ // Code-Review will be sticky.
+ String label = LabelId.CODE_REVIEW;
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().updateLabelType(label, b -> b.setCopyAnyScore(true));
+ u.save();
+ }
+
+ PushOneCommit.Result r = createChange();
+
+ // Add a new vote by user
+ requestScopeOperations.setApiUser(user.id());
+ gApi.changes().id(r.getChangeId()).current().review(ReviewInput.recommend());
+ requestScopeOperations.setApiUser(admin.id());
+
+ // Make a new patchset, keeping the Code-Review +1 vote.
+ amendChange(r.getChangeId());
+ assertVotes(detailedChange(r.getChangeId()), user, label, 1, null);
+
+ gApi.changes().id(r.getChangeId()).reviewer(user.email()).remove();
+
+ assertThat(r.getChange().notes().getApprovalsWithCopied()).isEmpty();
+
+ // Changes message has info about vote removed.
+ assertThat(Iterables.getLast(gApi.changes().id(r.getChangeId()).messages()).message)
+ .contains("Code-Review+1 by User");
+ }
+
private void assertChangeKindCacheContains(ObjectId prior, ObjectId next) {
ChangeKind kind =
changeKindCache.getIfPresent(ChangeKindCacheImpl.Key.create(prior, next, "recursive"));
@@ -1352,4 +1500,29 @@
assertThat(approval.value()).isEqualTo(value);
assertThat(approval.copied()).isEqualTo(copied);
}
+
+ /**
+ * Test submit rule that always return a passing record with a "Verified" label applied by {@link
+ * TestSubmitRule#userAccountId}.
+ */
+ private static class TestSubmitRule implements SubmitRule {
+ Account.Id userAccountId;
+
+ TestSubmitRule(Account.Id userAccountId) {
+ this.userAccountId = userAccountId;
+ }
+
+ @Override
+ public Optional<SubmitRecord> evaluate(ChangeData changeData) {
+ SubmitRecord record = new SubmitRecord();
+ record.ruleName = "testSubmitRule";
+ record.status = SubmitRecord.Status.OK;
+ SubmitRecord.Label label = new SubmitRecord.Label();
+ label.label = "Verified";
+ label.status = SubmitRecord.Label.Status.OK;
+ label.appliedBy = userAccountId;
+ record.labels = Arrays.asList(label);
+ return Optional.of(record);
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java
index 5124d11..29fdc15 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitWithStickyApprovalDiffIT.java
@@ -19,6 +19,7 @@
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.server.project.testing.TestLabels.labelBuilder;
import static com.google.gerrit.server.project.testing.TestLabels.value;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static org.eclipse.jgit.lib.Constants.HEAD;
import com.google.common.collect.ImmutableList;
@@ -35,6 +36,7 @@
import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
+import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.server.project.testing.TestLabels;
import com.google.inject.Inject;
import java.util.HashSet;
@@ -345,8 +347,8 @@
}
@Test
- @GerritConfig(name = "change.cumulativeCommentSizeLimit", value = "1k")
- public void autoGeneratedPostSubmitDiffIsNotPartOfTheCommentSizeLimit() throws Exception {
+ @GerritConfig(name = "change.cumulativeCommentSizeLimit", value = "10k")
+ public void autoGeneratedPostSubmitDiffIsPartOfTheCommentSizeLimit() throws Exception {
Change.Id changeId =
changeOperations.newChange().project(project).file("file").content("content").create();
gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
@@ -356,35 +358,69 @@
// Post a submit diff that is almost the cumulativeCommentSizeLimit
gApi.changes().id(changeId.get()).current().submit();
assertThat(Iterables.getLast(gApi.changes().id(changeId.get()).messages()).message)
- .doesNotContain("many unreviewed changes");
+ .doesNotContain("The diff is too large to show. Please review the diff");
- // unrelated comment and change message posting works fine, since the post submit diff is not
+ // unrelated comment and change message posting doesn't work, since the post submit diff is
// counted towards the cumulativeCommentSizeLimit for unrelated follow-up comments.
- // 800 + 400 + 400 > 1k, but 400 + 400 < 1k, hence these comments are accepted (the original
- // 800 is not counted).
- String message = new String(new char[400]).replace("\0", "a");
+ // 800 + 9500 > 10k.
+ String message = new String(new char[9500]).replace("\0", "a");
ReviewInput reviewInput = new ReviewInput().message(message);
CommentInput commentInput = new CommentInput();
commentInput.line = 1;
- commentInput.message = message;
commentInput.path = "file";
reviewInput.comments = ImmutableMap.of("file", ImmutableList.of(commentInput));
- gApi.changes().id(changeId.get()).current().review(reviewInput);
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.changes().id(changeId.get()).current().review(reviewInput));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("Exceeding maximum cumulative size of comments and change messages");
}
@Test
- @GerritConfig(name = "change.cumulativeCommentSizeLimit", value = "1k")
public void postSubmitDiffCannotBeTooBig() throws Exception {
Change.Id changeId =
changeOperations.newChange().project(project).file("file").content("content").create();
gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
- String content = new String(new char[1100]).replace("\0", "a");
+ // max post submit diff size is 300k
+ String content = new String(new char[320000]).replace("\0", "a");
changeOperations.change(changeId).newPatchset().file("file").content(content).create();
- // Post submit diff is over the cumulativeCommentSizeLimit, so we shorten the message.
+ // Post submit diff is over the postSubmitDiffSizeLimit (300k).
+ gApi.changes().id(changeId.get()).current().submit();
+ assertThat(Iterables.getLast(gApi.changes().id(changeId.get()).messages()).message)
+ .isEqualTo(
+ "Change has been successfully merged\n\n1 is the latest approved patch-set.\nThe "
+ + "change was submitted with unreviewed changes in the following "
+ + "files:\n\n```\nThe name of the file: file\nInsertions: 1, Deletions: 1.\n\nThe"
+ + " diff is too large to show. Please review the diff.\n```\n");
+ }
+
+ @Test
+ @GerritConfig(name = "change.cumulativeCommentSizeLimit", value = "10k")
+ public void postSubmitDiffCannotBeTooBigWithLargeComments() throws Exception {
+ Change.Id changeId =
+ changeOperations.newChange().project(project).file("file").content("content").create();
+ gApi.changes().id(changeId.get()).current().review(ReviewInput.approve());
+
+ // unrelated comment taking up most of the space, making post submit diff shorter.
+ String message = new String(new char[9700]).replace("\0", "a");
+ ReviewInput reviewInput = new ReviewInput().message(message);
+ CommentInput commentInput = new CommentInput();
+ commentInput.line = 1;
+ commentInput.path = "file";
+ reviewInput.comments = ImmutableMap.of("file", ImmutableList.of(commentInput));
+ gApi.changes().id(changeId.get()).current().review(reviewInput);
+
+ String content = new String(new char[500]).replace("\0", "a");
+ changeOperations.change(changeId).newPatchset().file("file").content(content).create();
+
+ // Post submit diff is over the cumulativeCommentSizeLimit, since the comment took most of
+ // the space (even though the post submit diff is not limited).
gApi.changes().id(changeId.get()).current().submit();
assertThat(Iterables.getLast(gApi.changes().id(changeId.get()).messages()).message)
.isEqualTo(
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupAssert.java b/javatests/com/google/gerrit/acceptance/api/group/GroupAssert.java
index 079d43e9..27f6111 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupAssert.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupAssert.java
@@ -42,7 +42,7 @@
assertThat(toBoolean(info.options.visibleToAll)).isEqualTo(group.isVisibleToAll());
assertThat(info.description).isEqualTo(group.getDescription());
assertThat(Url.decode(info.ownerId)).isEqualTo(group.getOwnerGroupUUID().get());
- assertThat(info.createdOn).isEqualTo(group.getCreatedOn());
+ assertThat(info.createdOn.toInstant()).isEqualTo(group.getCreatedOn());
}
public static boolean toBoolean(Boolean b) {
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 4cbc36b..63b67f8 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -109,8 +109,10 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
@@ -557,14 +559,17 @@
assertThrows(AuthException.class, () -> gApi.groups().create(name("newGroup")));
}
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
@Test
public void createdOnFieldIsPopulatedForNewGroup() throws Exception {
// NoteDb allows only second precision.
- Timestamp testStartTime = TimeUtil.truncateToSecond(TimeUtil.nowTs());
+ Instant testStartTime = TimeUtil.truncateToSecond(TimeUtil.now());
String newGroupName = name("newGroup");
GroupInfo group = gApi.groups().create(newGroupName).get();
- assertThat(group.createdOn).isAtLeast(testStartTime);
+ assertThat(group.createdOn.toInstant()).isAtLeast(testStartTime);
}
@Test
@@ -606,7 +611,7 @@
assertThat(group.name).isEqualTo(anonymousUsersGroup.getName());
group = gApi.groups().id(anonymousUsersGroup.getName()).get();
- assertThat(group.id).isEqualTo(Url.encode((anonymousUsersGroup.getUUID().get())));
+ assertThat(group.id).isEqualTo(Url.encode(anonymousUsersGroup.getUUID().get()));
}
@Test
@@ -614,7 +619,7 @@
GroupReference anonymousUsersGroup = systemGroupBackend.getGroup(ANONYMOUS_USERS);
GroupInfo group = gApi.groups().id("Anonymous Users").get();
assertThat(group.name).isEqualTo(anonymousUsersGroup.getName());
- assertThat(group.id).isEqualTo(Url.encode((anonymousUsersGroup.getUUID().get())));
+ assertThat(group.id).isEqualTo(Url.encode(anonymousUsersGroup.getUUID().get()));
}
@Test
@@ -1603,6 +1608,9 @@
return createCommit(repo, commitMessage, null);
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
private ObjectId createCommit(Repository repo, String commitMessage, @Nullable ObjectId treeId)
throws IOException {
try (ObjectInserter oi = repo.newObjectInserter()) {
@@ -1610,7 +1618,7 @@
treeId = oi.insert(Constants.OBJ_TREE, new byte[] {});
}
- PersonIdent ident = new PersonIdent(serverIdent.get(), TimeUtil.nowTs());
+ PersonIdent ident = new PersonIdent(serverIdent.get(), Date.from(TimeUtil.now()));
CommitBuilder cb = new CommitBuilder();
cb.setTreeId(treeId);
cb.setCommitter(ident);
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/PortedCommentsIT.java b/javatests/com/google/gerrit/acceptance/api/revision/PortedCommentsIT.java
index 7197425..ba45fb2 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/PortedCommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/PortedCommentsIT.java
@@ -22,23 +22,31 @@
import static com.google.gerrit.extensions.common.testing.CommentInfoSubject.assertThatList;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static com.google.gerrit.truth.MapSubject.assertThatMap;
+import static java.util.stream.Collectors.joining;
+import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.truth.Correspondence;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.acceptance.testsuite.change.TestCommentCreation;
import com.google.gerrit.acceptance.testsuite.change.TestPatchset;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.extensions.api.changes.DeleteCommentInput;
+import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.truth.NullAwareCorrespondence;
import com.google.inject.Inject;
@@ -47,11 +55,12 @@
import java.util.Collection;
import java.util.List;
import java.util.Map;
+import java.util.stream.IntStream;
+import org.eclipse.jgit.lib.ObjectId;
import org.junit.Ignore;
import org.junit.Test;
public class PortedCommentsIT extends AbstractDaemonTest {
-
@Inject private ChangeOperations changeOps;
@Inject private AccountOperations accountOps;
@Inject private RequestScopeOperations requestScopeOps;
@@ -143,6 +152,39 @@
}
@Test
+ public void commentsArePortedWhenAllEditsAreDueToRebase() throws Exception {
+ String fileName = "f.txt";
+ String baseContent =
+ IntStream.rangeClosed(1, 50)
+ .mapToObj(number -> String.format("Line %d\n", number))
+ .collect(joining());
+ ObjectId headCommit = testRepo.getRepository().resolve("HEAD");
+ ObjectId baseCommit = addCommit(headCommit, fileName, baseContent);
+
+ // Create a change on top of baseCommit, modify line 1, then add comment on line 10.
+ PushOneCommit.Result r = createEmptyChange();
+ Change.Id changeId = r.getChange().getId();
+ addModifiedPatchSet(
+ changeId.toString(), fileName, baseContent.replace("Line 1\n", "Line one\n"));
+ PatchSet.Id ps2Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+ newComment(ps2Id).message("Line comment").onLine(10).ofFile(fileName).create();
+
+ // Add a commit on top of baseCommit. Delete line 4. Rebase the change on top of this commit.
+ ObjectId newBase = addCommit(baseCommit, fileName, baseContent.replace("Line 4\n", ""));
+ rebaseChangeOn(changeId.toString(), newBase);
+ PatchSet.Id ps3Id = changeOps.change(changeId).currentPatchset().get().patchsetId();
+
+ List<CommentInfo> portedComments = flatten(getPortedComments(ps3Id));
+ assertThat(portedComments).hasSize(1);
+ int portedLine = portedComments.get(0).line;
+ BinaryResult fileContent = gApi.changes().id(changeId.get()).current().file(fileName).content();
+ List<String> lines = Splitter.on("\n").splitToList(fileContent.asString());
+ // Comment has shifted to L9 instead of L10 because of the deletion of line 4.
+ assertThat(portedLine).isEqualTo(9);
+ assertThat(lines.get(portedLine - 1)).isEqualTo("Line 10");
+ }
+
+ @Test
public void resolvedAndUnresolvedDraftCommentsArePorted() throws Exception {
Account.Id accountId = accountOps.newAccount().create();
// Set up change and patchsets.
@@ -1803,7 +1845,7 @@
}
@Test
- public void deletedCommentContentIsNotCachedInPortedComments() throws Exception {
+ public void deletedCommentContentIsNotPorted() throws Exception {
// Set up change and patchsets.
Change.Id changeId = changeOps.newChange().create();
PatchSet.Id patchsetId1 = changeOps.change(changeId).currentPatchset().get().patchsetId();
@@ -1817,9 +1859,9 @@
.revision(patchsetId1.get())
.comment(commentUuid)
.delete(new DeleteCommentInput());
- CommentInfo portedComment = getPortedComment(patchsetId2, commentUuid);
+ List<CommentInfo> portedComments = flatten(getPortedComments(patchsetId2));
- assertThat(portedComment).message().doesNotContain("Confidential content");
+ assertThatList(portedComments).isEmpty();
}
@Test
@@ -1897,9 +1939,7 @@
*/
private static ImmutableList<CommentInfo> flatten(
Map<String, List<CommentInfo>> commentsPerFile) {
- return commentsPerFile.values().stream()
- .flatMap(Collection::stream)
- .collect((toImmutableList()));
+ return commentsPerFile.values().stream().flatMap(Collection::stream).collect(toImmutableList());
}
// Unfortunately, we don't get an absolutely helpful error message when using this correspondence
@@ -1909,4 +1949,35 @@
private static Correspondence<CommentInfo, String> hasUuid() {
return NullAwareCorrespondence.transforming(comment -> comment.id, "hasUuid");
}
+
+ private Result createEmptyChange() throws Exception {
+ PushOneCommit push =
+ pushFactory.create(admin.newIdent(), testRepo, "Test change", ImmutableMap.of());
+ return push.to("refs/for/master");
+ }
+
+ private void rebaseChangeOn(String changeId, ObjectId newParent) throws Exception {
+ RebaseInput rebaseInput = new RebaseInput();
+ rebaseInput.base = newParent.getName();
+ gApi.changes().id(changeId).current().rebase(rebaseInput);
+ }
+
+ private void addModifiedPatchSet(String changeId, String filePath, String content)
+ throws Exception {
+ gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(content));
+ gApi.changes().id(changeId).edit().publish();
+ }
+
+ private ObjectId addCommit(ObjectId parentCommit, String fileName, String fileContent)
+ throws Exception {
+ testRepo.reset(parentCommit);
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(),
+ testRepo,
+ "Adjust files of repo",
+ ImmutableMap.of(fileName, fileContent));
+ PushOneCommit.Result result = push.to("refs/for/master");
+ return result.getCommit();
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index a6666d4..dcd274d 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -51,12 +51,13 @@
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.webui.EditWebLink;
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 java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.text.SimpleDateFormat;
+import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -148,7 +149,7 @@
Map<String, FileDiffOutput> modifiedFiles =
diffOperations.listModifiedFilesAgainstParent(
- project, result.getCommit(), /* parentNum= */ 0);
+ project, result.getCommit(), /* parentNum= */ 0, DiffOptions.DEFAULTS);
assertThat(modifiedFiles.keySet()).containsExactly("/COMMIT_MSG", "f.txt");
assertThat(
@@ -2968,6 +2969,9 @@
return "An unchanged patchset\n\nChange-Id: " + changeId;
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
private void assertDiffForNewFile(
PushOneCommit.Result pushResult, String path, String expectedContentSideB) throws Exception {
DiffInfo diff =
@@ -2986,16 +2990,18 @@
abbreviateName(parentCommit, 8, testRepo.getRevWalk().getObjectReader());
headers.add("Parent: " + parentCommitId + " (" + parentCommit.getShortMessage() + ")");
- SimpleDateFormat dtfmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US);
PersonIdent author = c.getAuthorIdent();
- dtfmt.setTimeZone(author.getTimeZone());
+ DateTimeFormatter fmt =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z")
+ .withLocale(Locale.US)
+ .withZone(author.getTimeZone().toZoneId());
headers.add("Author: " + author.getName() + " <" + author.getEmailAddress() + ">");
- headers.add("AuthorDate: " + dtfmt.format(author.getWhen().getTime()));
+ headers.add("AuthorDate: " + fmt.format(author.getWhen().toInstant()));
PersonIdent committer = c.getCommitterIdent();
- dtfmt.setTimeZone(committer.getTimeZone());
+ fmt = fmt.withZone(committer.getTimeZone().toZoneId());
headers.add("Commit: " + committer.getName() + " <" + committer.getEmailAddress() + ">");
- headers.add("CommitDate: " + dtfmt.format(committer.getWhen().getTime()));
+ headers.add("CommitDate: " + fmt.format(committer.getWhen().toInstant()));
headers.add("");
}
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index d3fe83f..72b5f93 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -100,7 +100,6 @@
import com.google.gerrit.testing.FakeEmailSender;
import com.google.inject.Inject;
import java.io.ByteArrayOutputStream;
-import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
@@ -1683,10 +1682,15 @@
}
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+ // Instant
+ @SuppressWarnings("JdkObsolete")
private void assertPersonIdent(GitPerson gitPerson, PersonIdent expectedIdent) {
assertThat(gitPerson.name).isEqualTo(expectedIdent.getName());
assertThat(gitPerson.email).isEqualTo(expectedIdent.getEmailAddress());
- assertThat(gitPerson.date).isEqualTo(new Timestamp(expectedIdent.getWhen().getTime()));
+ assertThat(gitPerson.date.getTime()).isEqualTo(expectedIdent.getWhen().getTime());
assertThat(gitPerson.tz).isEqualTo(expectedIdent.getTimeZoneOffset());
}
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 2253202..ba1e1a7 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -1427,6 +1427,34 @@
}
@Test
+ public void pushToNonVisibleBranchIsRejected() throws Exception {
+ String master = "refs/heads/master";
+
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(block(Permission.READ).ref(master).group(REGISTERED_USERS))
+ .update();
+
+ testRepo.branch("HEAD").commit().message("New Commit 1").insertChangeId().create();
+ // Since the branch is not visible to the caller, the command tries to create the ref resulting
+ // in the command being rejected because the ref already exists.
+ assertPushRejected(
+ pushHead(testRepo, master),
+ master,
+ "Cannot create ref 'refs/heads/master' because it already exists.");
+
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.READ).ref(master).group(REGISTERED_USERS))
+ .update();
+
+ testRepo.branch("HEAD").commit().message("New Commit 2").insertChangeId().create();
+ assertPushOk(pushHead(testRepo, master), master);
+ }
+
+ @Test
public void pushSameCommitTwiceUsingMagicBranchBaseOption() throws Exception {
projectOperations
.project(project)
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index 194f5f9..ef9f004 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -1397,12 +1397,13 @@
String patchSetRef = change.getPatchSetId().toRefName();
try (AutoCloseable ignored = disableChangeIndex();
Repository repo = repoManager.openRepository(project)) {
- Collection<Ref> singleRef = ImmutableList.of(repo.exactRef(patchSetRef));
- Collection<Ref> filteredRefs =
- permissionBackend
- .user(user(admin))
- .project(project)
- .filter(singleRef, repo, RefFilterOptions.defaults());
+ ImmutableList<Ref> singleRef = ImmutableList.of(repo.exactRef(patchSetRef));
+ ImmutableList<Ref> filteredRefs =
+ ImmutableList.copyOf(
+ permissionBackend
+ .user(user(admin))
+ .project(project)
+ .filter(singleRef, repo, RefFilterOptions.defaults()));
assertThat(filteredRefs).isEqualTo(singleRef);
}
}
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index 8367f60..14a8e98 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -24,7 +24,6 @@
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.testsuite.ThrowingConsumer;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
-import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -35,7 +34,6 @@
import com.google.gerrit.testing.ConfigSuite;
import com.google.inject.Inject;
import java.util.ArrayDeque;
-import java.util.Map;
import org.apache.commons.lang.RandomStringUtils;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
@@ -144,7 +142,6 @@
gApi.changes().id(id2).current().review(ReviewInput.approve());
gApi.changes().id(id3).current().review(ReviewInput.approve());
- Map<BranchNameKey, ObjectId> preview = fetchFromSubmitPreview(id1);
gApi.changes().id(id1).current().submit();
ObjectId subRepoId =
subRepo
@@ -156,25 +153,6 @@
.getObjectId();
expectToHaveSubmoduleState(superRepo, "master", subKey, subRepoId);
-
- // As the submodules have changed commits, the superproject tree will be
- // different, so we cannot directly compare the trees here, so make
- // assumptions only about the changed branches:
- assertThat(preview).containsKey(BranchNameKey.create(superKey, "refs/heads/master"));
- assertThat(preview).containsKey(BranchNameKey.create(subKey, "refs/heads/master"));
-
- if ((getSubmitType() == SubmitType.CHERRY_PICK)
- || (getSubmitType() == SubmitType.REBASE_ALWAYS)) {
- // each change is updated and the respective target branch is updated:
- assertThat(preview).hasSize(5);
- } else if ((getSubmitType() == SubmitType.REBASE_IF_NECESSARY)) {
- // Either the first is used first as is, then the second and third need
- // rebasing, or those two stay as is and the first is rebased.
- // add in 2 master branches, expect 3 or 4:
- assertThat(preview.size()).isAnyOf(3, 4);
- } else {
- assertThat(preview).hasSize(2);
- }
}
@Test
@@ -661,12 +639,6 @@
}
@Test
- public void branchCircularSubscriptionPreview() throws Exception {
- testBranchCircularSubscription(
- changeId -> gApi.changes().id(changeId).current().submitPreview());
- }
-
- @Test
public void projectCircularSubscriptionWholeTopic() throws Exception {
allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
allowMatchingSubmoduleSubscription(superKey, "refs/heads/dev", subKey, "refs/heads/dev");
diff --git a/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
index ed5e559..c868d0b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/CancellationIT.java
@@ -35,6 +35,7 @@
import com.google.gerrit.server.validators.ProjectCreationValidationListener;
import com.google.gerrit.server.validators.ValidationException;
import com.google.inject.Inject;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import org.apache.http.message.BasicHeader;
@@ -194,6 +195,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
public void abortIfServerDeadlineExceeded() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
assertThat(response.getEntityContent()).isEqualTo("Server Deadline Exceeded\n\ntimeout=1ms");
@@ -203,6 +205,7 @@
@GerritConfig(name = "deadline.foo.timeout", value = "1ms")
@GerritConfig(name = "deadline.bar.timeout", value = "100ms")
public void stricterDeadlineTakesPrecedence() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
assertThat(response.getEntityContent())
@@ -213,6 +216,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.requestType", value = "REST")
public void abortIfServerDeadlineExceeded_requestType() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
assertThat(response.getEntityContent())
@@ -223,6 +227,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.requestUriPattern", value = "/projects/.*")
public void abortIfServerDeadlineExceeded_requestUriPattern() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
assertThat(response.getEntityContent())
@@ -235,6 +240,7 @@
name = "deadline.default.excludedRequestUriPattern",
value = "/projects/non-matching")
public void abortIfServerDeadlineExceeded_excludedRequestUriPattern() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
assertThat(response.getEntityContent())
@@ -249,6 +255,7 @@
value = "/projects/non-matching")
public void abortIfServerDeadlineExceeded_requestUriPatternAndExcludedRequestUriPattern()
throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
assertThat(response.getEntityContent())
@@ -259,6 +266,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.projectPattern", value = ".*new.*")
public void abortIfServerDeadlineExceeded_projectPattern() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
assertThat(response.getEntityContent())
@@ -269,6 +277,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.account", value = "1000000")
public void abortIfServerDeadlineExceeded_account() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
assertThat(response.getEntityContent())
@@ -279,6 +288,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.requestType", value = "SSH")
public void nonMatchingServerDeadlineIsIgnored_requestType() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -287,6 +297,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.requestUriPattern", value = "/changes/.*")
public void nonMatchingServerDeadlineIsIgnored_requestUriPattern() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -295,6 +306,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.excludedRequestUriPattern", value = "/projects/.*")
public void nonMatchingServerDeadlineIsIgnored_excludedRequestUriPattern() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -305,6 +317,7 @@
@GerritConfig(name = "deadline.default.excludedRequestUriPattern", value = "/projects/.*new")
public void nonMatchingServerDeadlineIsIgnored_requestUriPatternAndExcludedRequestUriPattern()
throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -313,6 +326,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.projectPattern", value = ".*foo.*")
public void nonMatchingServerDeadlineIsIgnored_projectPattern() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -321,6 +335,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.account", value = "999")
public void nonMatchingServerDeadlineIsIgnored_account() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -329,6 +344,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.isAdvisory", value = "true")
public void advisoryServerDeadlineIsIgnored() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -338,6 +354,7 @@
@GerritConfig(name = "deadline.test.isAdvisory", value = "true")
@GerritConfig(name = "deadline.default.timeout", value = "2ms")
public void nonAdvisoryDeadlineIsAppliedIfStricterAdvisoryDeadlineExists() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(4));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
assertThat(response.getStatusCode()).isEqualTo(SC_REQUEST_TIMEOUT);
assertThat(response.getEntityContent())
@@ -347,6 +364,7 @@
@Test
@GerritConfig(name = "deadline.default.timeout", value = "1")
public void invalidServerDeadlineIsIgnored_missingTimeUnit() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -354,6 +372,7 @@
@Test
@GerritConfig(name = "deadline.default.timeout", value = "1x")
public void invalidServerDeadlineIsIgnored_invalidTimeUnit() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -369,6 +388,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.requestType", value = "INVALID")
public void invalidServerDeadlineIsIgnored_invalidRequestType() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -377,6 +397,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.requestUriPattern", value = "][")
public void invalidServerDeadlineIsIgnored_invalidRequestUriPattern() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -385,6 +406,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.excludedRequestUriPattern", value = "][")
public void invalidServerDeadlineIsIgnored_invalidExcludedRequestUriPattern() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -393,6 +415,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.projectPattern", value = "][")
public void invalidServerDeadlineIsIgnored_invalidProjectPattern() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -401,6 +424,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.account", value = "invalid")
public void invalidServerDeadlineIsIgnored_invalidAccount() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -416,6 +440,7 @@
@GerritConfig(name = "deadline.default.timeout", value = "0ms")
@GerritConfig(name = "deadline.default.requestType", value = "REST")
public void deadlineConfigWithZeroTimeoutIsIgnored() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response = adminRestSession.putWithHeaders("/projects/" + name("new"));
response.assertCreated();
}
@@ -449,6 +474,7 @@
@Test
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
public void clientProvidedDeadlineOverridesServerDeadline() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response =
adminRestSession.putWithHeaders(
"/projects/" + name("new"), new BasicHeader(RestApiServlet.X_GERRIT_DEADLINE, "2ms"));
@@ -460,6 +486,7 @@
@Test
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
public void clientCanDisableDeadlineBySettingZeroAsDeadline() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
RestResponse response =
adminRestSession.putWithHeaders(
"/projects/" + name("new"), new BasicHeader(RestApiServlet.X_GERRIT_DEADLINE, "0"));
@@ -574,6 +601,7 @@
@Test
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
public void abortPushIfServerDeadlineExceeded() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
PushOneCommit.Result r = push.to("refs/for/master");
r.assertErrorStatus("Server Deadline Exceeded (default.timeout=1ms)");
@@ -582,6 +610,7 @@
@Test
@GerritConfig(name = "receive.timeout", value = "1ms")
public void abortPushIfTimeoutExceeded() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
PushOneCommit.Result r = push.to("refs/for/master");
r.assertErrorStatus("Server Deadline Exceeded (receive.timeout=1ms)");
@@ -591,6 +620,7 @@
@GerritConfig(name = "receive.timeout", value = "1ms")
@GerritConfig(name = "deadline.default.timeout", value = "10s")
public void receiveTimeoutTakesPrecedence() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
PushOneCommit.Result r = push.to("refs/for/master");
r.assertErrorStatus("Server Deadline Exceeded (receive.timeout=1ms)");
@@ -649,6 +679,7 @@
@Test
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
public void clientProvidedDeadlineOnPushOverridesServerDeadline() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
List<String> pushOptions = new ArrayList<>();
pushOptions.add("deadline=2ms");
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
@@ -660,6 +691,7 @@
@Test
@GerritConfig(name = "receive.timeout", value = "1ms")
public void clientProvidedDeadlineOnPushDoesntOverrideServerTimeout() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
List<String> pushOptions = new ArrayList<>();
pushOptions.add("deadline=10m");
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
@@ -671,6 +703,7 @@
@Test
@GerritConfig(name = "deadline.default.timeout", value = "1ms")
public void clientCanDisableDeadlineOnPushBySettingZeroAsDeadline() throws Exception {
+ testTicker.useFakeTicker().setAutoIncrementStep(Duration.ofMillis(2));
List<String> pushOptions = new ArrayList<>();
pushOptions.add("deadline=0");
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java b/javatests/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
index be4fde0..e1e5b85 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
@@ -57,7 +57,7 @@
RestResponse r = userRestSession.get("/accounts/self/capabilities");
r.assertOK();
CapabilityInfo info =
- (new Gson()).fromJson(r.getReader(), new TypeToken<CapabilityInfo>() {}.getType());
+ new Gson().fromJson(r.getReader(), new TypeToken<CapabilityInfo>() {}.getType());
for (String c : GlobalCapability.getAllNames()) {
if (ADMINISTRATE_SERVER.equals(c)) {
assertThat(info.administrateServer).isFalse();
@@ -87,7 +87,7 @@
RestResponse r = adminRestSession.get("/accounts/self/capabilities");
r.assertOK();
CapabilityInfo info =
- (new Gson()).fromJson(r.getReader(), new TypeToken<CapabilityInfo>() {}.getType());
+ new Gson().fromJson(r.getReader(), new TypeToken<CapabilityInfo>() {}.getType());
for (String c : GlobalCapability.getAllNames()) {
if (BATCH_CHANGES_LIMIT.equals(c)) {
// It does not have default value for any user as it can override the
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java b/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
index a1e9bf1..c89e11a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
@@ -59,7 +59,7 @@
AccountDetailInfo info = newGson().fromJson(r.getReader(), AccountDetailInfo.class);
assertAccountInfo(admin, info);
Account account = getAccount(admin.id());
- assertThat(info.registeredOn).isEqualTo(account.registeredOn());
+ assertThat(info.registeredOn.getTime()).isEqualTo(account.registeredOn().toEpochMilli());
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
index 79484ca..f3b13d2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
@@ -145,7 +145,6 @@
RestCall.get("/changes/%s/revisions/%s/related"),
RestCall.get("/changes/%s/revisions/%s/review"),
RestCall.post("/changes/%s/revisions/%s/review"),
- RestCall.get("/changes/%s/revisions/%s/preview_submit"),
RestCall.post("/changes/%s/revisions/%s/submit"),
RestCall.get("/changes/%s/revisions/%s/submit_type"),
RestCall.post("/changes/%s/revisions/%s/test.submit_rule"),
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index d967f48..de14d00 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -81,7 +81,6 @@
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.events.ChangeIndexedListener;
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.gerrit.extensions.webui.UiAction;
@@ -104,10 +103,8 @@
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
-import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import org.eclipse.jgit.diff.DiffFormatter;
@@ -148,162 +145,25 @@
assertThat(projectOperations.project(project).hasHead("master")).isFalse();
PushOneCommit.Result change = createChange();
assertThat(change.getCommit().getParents()).isEmpty();
- Map<BranchNameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
assertThat(projectOperations.project(project).hasHead("master")).isFalse();
- assertThat(actual).hasSize(1);
submit(change.getChangeId());
assertThat(projectOperations.project(project).getHead("master").getId())
.isEqualTo(change.getCommit());
- assertTrees(project, actual);
}
@Test
public void submitSingleChange() throws Throwable {
RevCommit initialHead = projectOperations.project(project).getHead("master");
PushOneCommit.Result change = createChange();
- Map<BranchNameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
assertThat(headAfterSubmit).isEqualTo(initialHead);
assertRefUpdatedEvents();
assertChangeMergedEvents();
- if ((getSubmitType() == SubmitType.CHERRY_PICK)
- || (getSubmitType() == SubmitType.REBASE_ALWAYS)) {
- // The change is updated as well:
- assertThat(actual).hasSize(2);
- } else {
- assertThat(actual).hasSize(1);
- }
-
submit(change.getChangeId());
- assertTrees(project, actual);
- }
-
- @Test
- public void submitMultipleChangesOtherMergeConflictPreview() throws Throwable {
- RevCommit initialHead = projectOperations.project(project).getHead("master");
-
- PushOneCommit.Result change = createChange("Change 1", "a.txt", "content");
- submit(change.getChangeId());
-
- RevCommit headAfterFirstSubmit = projectOperations.project(project).getHead("master");
- testRepo.reset(initialHead);
- PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "other content");
- PushOneCommit.Result change3 = createChange("Change 3", "d", "d");
- PushOneCommit.Result change4 = createChange("Change 4", "e", "e");
- // change 2 is not approved, but we ignore labels
- approve(change3.getChangeId());
-
- try (BinaryResult request =
- gApi.changes().id(change4.getChangeId()).current().submitPreview()) {
- assertThat(getSubmitType()).isEqualTo(SubmitType.CHERRY_PICK);
- submit(change4.getChangeId());
- } catch (RestApiException e) {
- switch (getSubmitType()) {
- case FAST_FORWARD_ONLY:
- assertThat(e.getMessage())
- .isEqualTo(
- "Failed to submit 3 changes due to the following problems:\n"
- + "Change "
- + change2.getChange().getId()
- + ": Project policy "
- + "requires all submissions to be a fast-forward. Please "
- + "rebase the change locally and upload again for review.\n"
- + "Change "
- + change3.getChange().getId()
- + ": Project policy "
- + "requires all submissions to be a fast-forward. Please "
- + "rebase the change locally and upload again for review.\n"
- + "Change "
- + change4.getChange().getId()
- + ": Project policy "
- + "requires all submissions to be a fast-forward. Please "
- + "rebase the change locally and upload again for review.");
- break;
- case REBASE_IF_NECESSARY:
- case REBASE_ALWAYS:
- String change2hash = change2.getChange().currentPatchSet().commitId().name();
- assertThat(e.getMessage())
- .isEqualTo(
- "Cannot rebase "
- + change2hash
- + ": The change could "
- + "not be rebased due to a conflict during merge.\n\n"
- + "merge conflict(s):\n"
- + "a.txt");
- break;
- case MERGE_ALWAYS:
- case MERGE_IF_NECESSARY:
- case INHERIT:
- assertThat(e.getMessage())
- .isEqualTo(
- "Failed to submit 3 changes due to the following problems:\n"
- + "Change "
- + change2.getChange().getId()
- + ": Change could not be "
- + "merged due to a path conflict. Please rebase the change "
- + "locally and upload the rebased commit for review.\n"
- + "Change "
- + change3.getChange().getId()
- + ": Change could not be "
- + "merged due to a path conflict. Please rebase the change "
- + "locally and upload the rebased commit for review.\n"
- + "Change "
- + change4.getChange().getId()
- + ": Change could not be "
- + "merged due to a path conflict. Please rebase the change "
- + "locally and upload the rebased commit for review.");
- break;
- case CHERRY_PICK:
- default:
- assertWithMessage("Should not reach here.").fail();
- break;
- }
-
- RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
- assertThat(headAfterSubmit).isEqualTo(headAfterFirstSubmit);
- assertRefUpdatedEvents(initialHead, headAfterFirstSubmit);
- assertChangeMergedEvents(change.getChangeId(), headAfterFirstSubmit.name());
- }
- }
-
- @Test
- public void submitMultipleChangesPreview() throws Throwable {
- RevCommit initialHead = projectOperations.project(project).getHead("master");
- PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "other content");
- PushOneCommit.Result change3 = createChange("Change 3", "d", "d");
- PushOneCommit.Result change4 = createChange("Change 4", "e", "e");
- // change 2 is not approved, but we ignore labels
- approve(change3.getChangeId());
- Map<BranchNameKey, ObjectId> actual = fetchFromSubmitPreview(change4.getChangeId());
- Map<String, Map<String, Integer>> expected = new HashMap<>();
- expected.put(project.get(), new HashMap<>());
- expected.get(project.get()).put("refs/heads/master", 3);
-
- assertThat(actual).containsKey(BranchNameKey.create(project, "refs/heads/master"));
- if (getSubmitType() == SubmitType.CHERRY_PICK) {
- // CherryPick ignores dependencies, thus only change and destination
- // branch refs are modified.
- assertThat(actual).hasSize(2);
- } else if (getSubmitType() == SubmitType.REBASE_ALWAYS) {
- // RebaseAlways takes care of dependencies, therefore Change{2,3,4} and
- // destination branch will be modified.
- assertThat(actual).hasSize(4);
- } else {
- assertThat(actual).hasSize(1);
- }
-
- // check that the submit preview did not actually submit
- RevCommit headAfterSubmit = projectOperations.project(project).getHead("master");
- assertThat(headAfterSubmit).isEqualTo(initialHead);
- assertRefUpdatedEvents();
- assertChangeMergedEvents();
-
- // now check we actually have the same content:
- approve(change2.getChangeId());
- submit(change4.getChangeId());
- assertTrees(project, actual);
+ headAfterSubmit = projectOperations.project(project).getHead("master");
+ assertThat(headAfterSubmit).isNotEqualTo(initialHead);
}
/**
@@ -1238,14 +1098,11 @@
assertThat(projectOperations.project(project).hasHead("master")).isFalse();
PushOneCommit.Result change = createChange();
assertThat(change.getCommit().getParents()).isEmpty();
- Map<BranchNameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
assertThat(projectOperations.project(project).hasHead("master")).isFalse();
- assertThat(actual).hasSize(1);
submit(change.getChangeId());
assertThat(projectOperations.project(project).getHead("master").getId())
.isEqualTo(change.getCommit());
- assertTrees(project, actual);
}
@Test
@@ -1259,20 +1116,17 @@
change.assertOkStatus();
assertThat(change.getCommit().getTree()).isEqualTo(EMPTY_TREE_ID);
- Map<BranchNameKey, ObjectId> actual = fetchFromSubmitPreview(change.getChangeId());
assertThat(projectOperations.project(project).hasHead("master")).isFalse();
- assertThat(actual).hasSize(1);
submit(change.getChangeId());
assertThat(projectOperations.project(project).getHead("master").getId())
.isEqualTo(change.getCommit());
- assertTrees(project, actual);
}
private void setChangeStatusToNew(PushOneCommit.Result... changes) throws Throwable {
for (PushOneCommit.Result change : changes) {
try (BatchUpdate bu =
- batchUpdateFactory.create(project, userFactory.create(admin.id()), TimeUtil.nowTs())) {
+ batchUpdateFactory.create(project, userFactory.create(admin.id()), TimeUtil.now())) {
bu.addOp(
change.getChange().getId(),
new BatchUpdateOp() {
@@ -1395,8 +1249,8 @@
submit(r.getChangeId());
assertThat(r.getChange().getMergedOn()).isPresent();
ChangeInfo change = gApi.changes().id(r.getChangeId()).get();
- assertThat(r.getChange().getMergedOn().get()).isEqualTo(change.updated);
- assertThat(r.getChange().getMergedOn().get()).isEqualTo(change.submitted);
+ assertThat(r.getChange().getMergedOn().get()).isEqualTo(change.getUpdated());
+ assertThat(r.getChange().getMergedOn().get()).isEqualTo(change.getSubmitted());
}
@Override
@@ -1506,8 +1360,12 @@
assertThat(actual.getTimeZone()).isEqualTo(expected.getTimeZone());
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
protected void assertAuthorAndCommitDateEquals(RevCommit commit) {
- assertThat(commit.getAuthorIdent().getWhen()).isEqualTo(commit.getCommitterIdent().getWhen());
+ assertThat(commit.getAuthorIdent().getWhen().getTime())
+ .isEqualTo(commit.getCommitterIdent().getWhen().getTime());
assertThat(commit.getAuthorIdent().getTimeZone())
.isEqualTo(commit.getCommitterIdent().getTimeZone());
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index e35f758..9fcce3d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -116,7 +116,11 @@
assertThat(info.enabled).isNull();
assertThat(info.label).isEqualTo("Submit whole topic");
assertThat(info.method).isEqualTo("POST");
- assertThat(info.title).matches("Change " + legacyId2 + " is not ready: needs Code-Review");
+ assertThat(info.title)
+ .matches(
+ "Change "
+ + legacyId2
+ + " is not ready: submit requirement 'Code-Review' is unsatisfied.");
} else {
noSubmitWholeTopicAssertions(actions, 1);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
index 0a11b15..b034a42 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
@@ -46,6 +46,7 @@
import com.google.gerrit.entities.Permission;
import com.google.gerrit.extensions.api.changes.AttentionSetInput;
import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
+import com.google.gerrit.extensions.api.changes.DeleteVoteInput;
import com.google.gerrit.extensions.api.changes.HashtagsInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewerInput;
@@ -261,6 +262,12 @@
fakeClock.now(), user.id(), AttentionSetUpdate.Operation.REMOVE, "removed");
assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate);
+ // The removal also shows up in AttentionSetInfo.
+ AttentionSetInfo attentionSetInfo =
+ Iterables.getOnlyElement(change(r).get().removedFromAttentionSet.values());
+ assertThat(attentionSetInfo.reason).isEqualTo("removed");
+ assertThat(attentionSetInfo.account).isEqualTo(getAccountInfo(user.id()));
+
// Second removal is ignored.
fakeClock.advance(Duration.ofSeconds(42));
change(r).attention(user.id().toString()).remove(new AttentionSetInput("removed again"));
@@ -1935,6 +1942,30 @@
}
@Test
+ public void deleteVotesDoesNotAffectAttentionSetWhenIgnoreAutomaticRulesIsSet() throws Exception {
+ PushOneCommit.Result r = createChange();
+
+ requestScopeOperations.setApiUser(user.id());
+ recommend(r.getChangeId());
+
+ requestScopeOperations.setApiUser(admin.id());
+
+ DeleteVoteInput deleteVoteInput = new DeleteVoteInput();
+ deleteVoteInput.label = LabelId.CODE_REVIEW;
+
+ // set this to true to not change the attention set.
+ deleteVoteInput.ignoreAutomaticAttentionSetRules = true;
+
+ gApi.changes()
+ .id(r.getChangeId())
+ .current()
+ .reviewer(user.id().toString())
+ .deleteVote(deleteVoteInput);
+
+ assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty();
+ }
+
+ @Test
public void deleteVotesOfOthersAddThemToAttentionSet() throws Exception {
PushOneCommit.Result r = createChange();
@@ -2018,7 +2049,7 @@
comment.side = Side.REVISION;
comment.path = Patch.COMMIT_MSG;
comment.message = "comment";
- comment.updated = TimeUtil.nowTs();
+ comment.setUpdated(TimeUtil.now());
comment.inReplyTo = id;
ReviewInput reviewInput = new ReviewInput();
reviewInput.comments = ImmutableMap.of(Patch.COMMIT_MSG, ImmutableList.of(comment));
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
index 88e5f10..70a3cf2 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
@@ -65,9 +65,9 @@
gApi.changes().id(r.getChangeId()).addReviewer(input);
ChangeInfo info = gApi.changes().id(r.getChangeId()).get(DETAILED_LABELS);
- assertThat(info.reviewers).isEqualTo(ImmutableMap.of(state, ImmutableList.of(acc)));
+ assertThat(info.reviewers).containsExactly(state, ImmutableList.of(acc));
// All reviewers added by email should be removable
- assertThat(info.removableReviewers).isEqualTo(ImmutableList.of(acc));
+ assertThat(info.removableReviewers).containsExactly(acc);
}
}
@@ -92,7 +92,7 @@
ChangeInfo info = gApi.changes().id(r.getChangeId()).get(DETAILED_LABELS);
assertThat(info.reviewers).isEqualTo(ImmutableMap.of(state, ImmutableList.of(byId, byEmail)));
// All reviewers (both by id and by email) should be removable
- assertThat(info.removableReviewers).isEqualTo(ImmutableList.of(byId, byEmail));
+ assertThat(info.removableReviewers).containsExactly(byId, byEmail);
}
}
@@ -368,6 +368,6 @@
}
private static String toRfcAddressString(AccountInfo info) {
- return (Address.create(info.name, info.email)).toString();
+ return Address.create(info.name, info.email).toString();
}
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index b0a14cf..dbebbf9 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -27,6 +27,7 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG;
+import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -593,7 +594,7 @@
PersonIdent expectedAuthor =
changeNoteUtil.newAccountIdIdent(
- getAccount(admin.id()).id(), c.created, serverIdent.get());
+ getAccount(admin.id()).id(), c.created.toInstant(), serverIdent.get());
assertThat(commit.getAuthorIdent()).isEqualTo(expectedAuthor);
assertThat(commit.getCommitterIdent())
@@ -1085,7 +1086,7 @@
ChangeInfo out = gApi.changes().create(in).get();
assertThat(out.project).isEqualTo(in.project);
assertThat(RefNames.fullName(out.branch)).isEqualTo(RefNames.fullName(in.branch));
- assertThat(out.subject).isEqualTo(in.subject.split("\n")[0]);
+ 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) {
@@ -1109,7 +1110,7 @@
ChangeInfo out = gApi.changes().createAsInfo(in);
assertThat(out.project).isEqualTo(in.project);
assertThat(RefNames.fullName(out.branch)).isEqualTo(RefNames.fullName(in.branch));
- assertThat(out.subject).isEqualTo(in.subject.split("\n")[0]);
+ 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) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
index 58e48e9..3be49df 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
@@ -99,7 +99,7 @@
"Failed to submit 2 changes due to the following problems:\n"
+ "Change "
+ id1
- + ": needs Code-Review");
+ + ": submit requirement 'Code-Review' is unsatisfied.");
RevCommit updatedHead = projectOperations.project(project).getHead("master");
assertThat(updatedHead.getId()).isEqualTo(initialHead.getId());
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index 157c93c..c4f8f2c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -38,22 +38,10 @@
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.inject.Inject;
-import java.io.File;
-import java.io.InputStream;
-import java.nio.file.Files;
-import java.util.ArrayList;
import java.util.List;
-import java.util.Map;
-import java.util.zip.GZIPInputStream;
-import org.apache.commons.compress.archivers.ArchiveStreamFactory;
-import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
-import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.RefSpec;
import org.junit.Test;
@@ -184,8 +172,6 @@
approve(change2b.getChangeId());
approve(change3.getChangeId());
- // get a preview before submitting:
- Map<BranchNameKey, ObjectId> preview = fetchFromSubmitPreview(change1b.getChangeId());
submit(change1b.getChangeId());
RevCommit tip1 = getRemoteLog(p1, "master").get(0);
@@ -197,23 +183,9 @@
if (isSubmitWholeTopicEnabled()) {
assertThat(tip2.getShortMessage()).isEqualTo(change2b.getCommit().getShortMessage());
assertThat(tip3.getShortMessage()).isEqualTo(change3.getCommit().getShortMessage());
-
- // check that the preview matched what happened:
- assertThat(preview).hasSize(3);
-
- assertThat(preview).containsKey(BranchNameKey.create(p1, "refs/heads/master"));
- assertTrees(p1, preview);
-
- assertThat(preview).containsKey(BranchNameKey.create(p2, "refs/heads/master"));
- assertTrees(p2, preview);
-
- assertThat(preview).containsKey(BranchNameKey.create(p3, "refs/heads/master"));
- assertTrees(p3, preview);
} else {
assertThat(tip2.getShortMessage()).isEqualTo(initialHead2.getShortMessage());
assertThat(tip3.getShortMessage()).isEqualTo(initialHead3.getShortMessage());
- assertThat(preview).hasSize(1);
- assertThat(preview.get(BranchNameKey.create(p1, "refs/heads/master"))).isNotNull();
}
}
@@ -281,13 +253,6 @@
+ "merged due to a path conflict. Please rebase the change locally "
+ "and upload the rebased commit for review.";
- // Get a preview before submitting:
- RestApiException thrown =
- assertThrows(
- RestApiException.class,
- () -> gApi.changes().id(change1b.getChangeId()).current().submitPreview().close());
- assertThat(thrown.getMessage()).isEqualTo(msg);
-
submitWithConflict(change1b.getChangeId(), msg);
} else {
submit(change1b.getChangeId());
@@ -756,34 +721,4 @@
assertRefUpdatedEvents();
assertChangeMergedEvents();
}
-
- @Test
- public void testPreviewSubmitTgz() throws Throwable {
- Project.NameKey p1 = projectOperations.newProject().create();
-
- TestRepository<?> repo1 = cloneProject(p1);
- PushOneCommit.Result change1 = createChange(repo1, "master", "test", "a.txt", "1", "topic");
- approve(change1.getChangeId());
-
- // get a preview before submitting:
- File tempfile;
- try (BinaryResult request =
- gApi.changes().id(change1.getChangeId()).current().submitPreview("tgz")) {
- assertThat(request.getContentType()).isEqualTo("application/x-gzip");
- tempfile = File.createTempFile("test", null);
- request.writeTo(Files.newOutputStream(tempfile.toPath()));
- }
-
- InputStream is = new GZIPInputStream(Files.newInputStream(tempfile.toPath()));
-
- List<String> untarredFiles = new ArrayList<>();
- try (TarArchiveInputStream tarInputStream =
- (TarArchiveInputStream) new ArchiveStreamFactory().createArchiveInputStream("tar", is)) {
- TarArchiveEntry entry;
- while ((entry = (TarArchiveEntry) tarInputStream.getNextEntry()) != null) {
- untarredFiles.add(entry.getName());
- }
- }
- assertThat(untarredFiles).containsExactly(p1.get() + ".git");
- }
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
index aa93815..2eade27 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
@@ -33,7 +33,7 @@
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.Project.NameKey;
-import com.google.gerrit.exceptions.InternalServerWithUserMessageException;
+import com.google.gerrit.exceptions.MergeUpdateException;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.SubmitType;
@@ -136,8 +136,8 @@
ChangeMessageModifier modifier2 = (msg, orig, tip, dest) -> msg + "A-footer: value\n";
try (Registration registration =
extensionRegistry.newRegistration().add(modifier1).add(modifier2)) {
- InternalServerWithUserMessageException thrown =
- assertThrows(InternalServerWithUserMessageException.class, () -> submitWithRebase());
+ MergeUpdateException thrown =
+ assertThrows(MergeUpdateException.class, () -> submitWithRebase());
Throwable cause = Throwables.getRootCause(thrown);
assertThat(cause).isInstanceOf(RuntimeException.class);
assertThat(cause).hasMessageThat().isEqualTo("boom");
@@ -153,8 +153,8 @@
.newRegistration()
.add(modifier1, "modifier-1")
.add(modifier2, "modifier-2")) {
- InternalServerWithUserMessageException thrown =
- assertThrows(InternalServerWithUserMessageException.class, () -> submitWithRebase());
+ MergeUpdateException thrown =
+ assertThrows(MergeUpdateException.class, () -> submitWithRebase());
Throwable cause = Throwables.getRootCause(thrown);
assertThat(cause).isInstanceOf(RuntimeException.class);
assertThat(cause)
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
index a63d60a..0a9a098 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
@@ -306,7 +306,10 @@
private void assertChangeSetMergeable(ChangeData change, boolean expected)
throws MissingObjectException, IncorrectObjectTypeException, IOException,
PermissionBackendException {
- ChangeSet cs = mergeSuperSet.get().completeChangeSet(change.change(), user(admin));
+ ChangeSet cs =
+ mergeSuperSet
+ .get()
+ .completeChangeSet(change.change(), user(admin), /* includingTopicClosure= */ false);
assertThat(submit.unmergeableChanges(cs).isEmpty()).isEqualTo(expected);
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java b/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
index 531357a..793f256 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
@@ -22,6 +22,7 @@
import static com.google.gerrit.acceptance.rest.project.AbstractPushTag.TagType.ANNOTATED;
import static com.google.gerrit.acceptance.rest.project.AbstractPushTag.TagType.LIGHTWEIGHT;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+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;
@@ -29,6 +30,7 @@
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.testing.ConfigSuite;
@@ -191,33 +193,57 @@
pushTagDeletion(tagName, Status.OK);
}
+ @Test
+ public void pushToNonVisibleTagIsRejected() throws Exception {
+ allowTagCreation();
+ allowPushOnRefsTags();
+
+ String tagName = pushTagForExistingCommit(Status.OK);
+
+ removeReadFromRefsTags();
+ removeReadFromRefsHeads();
+
+ pushTag(
+ tagName,
+ /* newCommit= */ true,
+ /* force= */ false,
+ Status.REJECTED_OTHER_REASON,
+ /* expectedMessage= */ String.format(
+ "Cannot create ref '%s' because it already exists.", tagRef(tagName)));
+ }
+
private String pushTagForExistingCommit(Status expectedStatus) throws Exception {
- return pushTag(null, false, false, expectedStatus);
+ return pushTag(null, false, false, expectedStatus, /* expectedMessage= */ null);
}
private String pushTagForNewCommit(Status expectedStatus) throws Exception {
- return pushTag(null, true, false, expectedStatus);
+ return pushTag(null, true, false, expectedStatus, /* expectedMessage= */ null);
}
private void fastForwardTagToExistingCommit(String tagName, Status expectedStatus)
throws Exception {
- pushTag(tagName, false, false, expectedStatus);
+ pushTag(tagName, false, false, expectedStatus, /* expectedMessage= */ null);
}
private void fastForwardTagToNewCommit(String tagName, Status expectedStatus) throws Exception {
- pushTag(tagName, true, false, expectedStatus);
+ pushTag(tagName, true, false, expectedStatus, /* expectedMessage= */ null);
}
private void forceUpdateTagToExistingCommit(String tagName, Status expectedStatus)
throws Exception {
- pushTag(tagName, false, true, expectedStatus);
+ pushTag(tagName, false, true, expectedStatus, /* expectedMessage= */ null);
}
private void forceUpdateTagToNewCommit(String tagName, Status expectedStatus) throws Exception {
- pushTag(tagName, true, true, expectedStatus);
+ pushTag(tagName, true, true, expectedStatus, /* expectedMessage= */ null);
}
- private String pushTag(String tagName, boolean newCommit, boolean force, Status expectedStatus)
+ private String pushTag(
+ String tagName,
+ boolean newCommit,
+ boolean force,
+ Status expectedStatus,
+ @Nullable String expectedMessage)
throws Exception {
if (force) {
testRepo.reset(initialHead);
@@ -256,6 +282,9 @@
: GitUtil.pushTag(testRepo, tagName, !createTag);
RemoteRefUpdate refUpdate = r.getRemoteUpdate(tagRef);
assertWithMessage(tagType.name()).that(refUpdate.getStatus()).isEqualTo(expectedStatus);
+ if (expectedMessage != null) {
+ assertWithMessage(tagType.name()).that(refUpdate.getMessage()).isEqualTo(expectedMessage);
+ }
return tagName;
}
@@ -352,6 +381,22 @@
.update();
}
+ private void removeReadFromRefsTags() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(block(Permission.READ).ref("refs/tags/*").group(REGISTERED_USERS))
+ .update();
+ }
+
+ private void removeReadFromRefsHeads() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(block(Permission.READ).ref("refs/heads/*").group(REGISTERED_USERS))
+ .update();
+ }
+
private void commit(PersonIdent ident, String subject) throws Exception {
commitBuilder().ident(ident).message(subject + " (" + System.nanoTime() + ")").create();
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java
index dfe69f9..755c2e1 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java
@@ -224,6 +224,19 @@
}
@Test
+ public void createWithNameAndDescription() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.description = "Foo label description";
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("Foo").create(input).get();
+
+ assertThat(createdLabel.name).isEqualTo("Foo");
+ assertThat(createdLabel.description).isEqualTo("Foo label description");
+ }
+
+ @Test
public void createWithNameAndValuesOnly() throws Exception {
LabelDefinitionInput input = new LabelDefinitionInput();
input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index ce92536..fdb2ed7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -37,6 +37,8 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.inject.Inject;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Repository;
import org.junit.Before;
import org.junit.Test;
@@ -192,6 +194,26 @@
assertThat(thrown).hasMessageThat().contains("not allowed to delete HEAD");
}
+ @Test
+ public void deleteRefsForBranch() throws Exception {
+ BranchNameKey refsForBranch = BranchNameKey.create(project, "refs/for/master");
+
+ // Creating a branch under refs/for/ is not allowed through the API, hence create it directly in
+ // the remote repo.
+ try (TestRepository<Repository> repo =
+ new TestRepository<>(repoManager.openRepository(project))) {
+ repo.branch(refsForBranch.branch()).commit().message("Initial empty commit").create();
+ }
+
+ assertThat(branch(refsForBranch).get().canDelete).isTrue();
+ String branchRev = branch(refsForBranch).get().revision;
+
+ branch(refsForBranch).delete();
+
+ eventRecorder.assertRefUpdatedEvents(project.get(), refsForBranch.branch(), branchRev, null);
+ assertThrows(ResourceNotFoundException.class, () -> branch(refsForBranch).get());
+ }
+
private void blockForcePush() throws Exception {
projectOperations
.project(project)
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListLabelsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListLabelsIT.java
index a397693..fbec664 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListLabelsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListLabelsIT.java
@@ -35,6 +35,7 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.inject.Inject;
import java.util.List;
+import java.util.Optional;
import org.junit.Test;
@NoHttpd
@@ -100,6 +101,24 @@
}
@Test
+ public void labelWithDescription() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig()
+ .updateLabelType(
+ "foo", labelType -> labelType.setDescription(Optional.of("foo label description")));
+ u.save();
+ }
+
+ List<LabelDefinitionInfo> labels = gApi.projects().name(project.get()).labels().get();
+ assertThat(labelNames(labels)).containsExactly("foo");
+
+ LabelDefinitionInfo fooLabel = Iterables.getOnlyElement(labels);
+ assertThat(fooLabel.description).isEqualTo("foo label description");
+ }
+
+ @Test
public void labelLimitedToBranches() throws Exception {
configLabel(
"foo", LabelFunction.NO_OP, ImmutableList.of("refs/heads/master", "^refs/heads/stable-.*"));
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index 2e274d9..c9c26f9 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -227,7 +227,7 @@
assertThatNameList(gApi.projects().list().withRegex(".*some").get())
.containsExactly(projectAwesome);
- String r = ("lpwr-some-project$").replace(".", "\\.");
+ String r = "lpwr-some-project$".replace(".", "\\.");
assertThatNameList(gApi.projects().list().withRegex(r).get()).containsExactly(someProject);
assertThatNameList(gApi.projects().list().withRegex(".*").get())
.containsExactly(
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java b/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java
index b4938c1..cfca936 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java
@@ -94,6 +94,20 @@
}
@Test
+ public void updateDescription() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.description = "Code review label description";
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(allProjects.get()).label(LabelId.CODE_REVIEW).update(input);
+ assertThat(updatedLabel.description).isEqualTo("Code review label description");
+
+ input.description = "";
+ updatedLabel = gApi.projects().name(allProjects.get()).label(LabelId.CODE_REVIEW).update(input);
+ assertThat(updatedLabel.description).isNull();
+ }
+
+ @Test
public void nameIsTrimmed() throws Exception {
LabelDefinitionInput input = new LabelDefinitionInput();
input.name = " Foo-Review ";
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
index b1879f6..8bf70f7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -41,7 +41,7 @@
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.inject.Inject;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -222,14 +222,14 @@
assertThat(result.ref).isEqualTo(R_TAGS + input.ref);
assertThat(result.revision).isEqualTo(input.revision);
assertThat(result.canDelete).isTrue();
- assertThat(result.created).isEqualTo(timestamp(r));
+ assertThat(result.created.toInstant()).isEqualTo(instant(r));
input.ref = "refs/tags/v2.0";
result = tag(input.ref).create(input).get();
assertThat(result.ref).isEqualTo(input.ref);
assertThat(result.revision).isEqualTo(input.revision);
assertThat(result.canDelete).isTrue();
- assertThat(result.created).isEqualTo(timestamp(r));
+ assertThat(result.created.toInstant()).isEqualTo(instant(r));
requestScopeOperations.setApiUser(user.id());
result = tag(input.ref).get();
@@ -457,8 +457,11 @@
return gApi.projects().name(project.get()).tag(tagname);
}
- private Timestamp timestamp(PushOneCommit.Result r) {
- return new Timestamp(r.getCommit().getCommitterIdent().getWhen().getTime());
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
+ private Instant instant(PushOneCommit.Result r) {
+ return r.getCommit().getCommitterIdent().getWhen().toInstant();
}
private void assertBadRequest(ListRefsRequest<TagInfo> req) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/server/approval/PatchSetApprovalUuidTest.java b/javatests/com/google/gerrit/acceptance/server/approval/PatchSetApprovalUuidTest.java
new file mode 100644
index 0000000..e1b4ccb
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/approval/PatchSetApprovalUuidTest.java
@@ -0,0 +1,41 @@
+package com.google.gerrit.acceptance.server.approval;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.server.approval.PatchSetApprovalUuidGenerator;
+import com.google.gerrit.server.approval.PatchSetApprovalUuidGeneratorImpl;
+import com.google.gerrit.server.util.time.TimeUtil;
+import java.time.Instant;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link PatchSetApprovalUuidGeneratorImpl} - the default implementation of {@link
+ * PatchSetApprovalUuidGenerator}.
+ */
+@RunWith(JUnit4.class)
+public class PatchSetApprovalUuidTest {
+
+ @Test
+ public void sameInput_differentUuid() {
+ PatchSetApprovalUuidGeneratorImpl patchSetApprovalUuidGenerator =
+ new PatchSetApprovalUuidGeneratorImpl();
+ for (short value = -2; value <= 2; value++) {
+ PatchSet.Id patchSetId = PatchSet.id(Change.id(1), 1);
+ Account.Id accountId = Account.id(1);
+ String label = LabelId.CODE_REVIEW;
+ Instant granted = TimeUtil.now();
+ PatchSetApproval.UUID uuid1 =
+ patchSetApprovalUuidGenerator.get(patchSetId, accountId, label, value, granted);
+ PatchSetApproval.UUID uuid2 =
+ patchSetApprovalUuidGenerator.get(patchSetId, accountId, label, value, granted);
+ assertThat(uuid2).isNotEqualTo(uuid1);
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 80cdad8..6d980c7 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -36,6 +36,7 @@
import com.google.gerrit.acceptance.testsuite.change.TestHumanComment;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.HumanComment;
@@ -47,9 +48,11 @@
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
+import com.google.gerrit.extensions.api.changes.ReviewerInput;
import com.google.gerrit.extensions.client.Comment;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -66,7 +69,7 @@
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.inject.Inject;
import com.google.inject.Provider;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -218,6 +221,30 @@
}
@Test
+ public void deletedCommentsAreResolved() throws Exception {
+ requestScopeOperations.setApiUser(admin.id());
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ String revId = r.getCommit().getName();
+ String commentMessage = "to be deleted";
+ CommentInput comment =
+ CommentsUtil.newComment(
+ COMMIT_MSG, Side.REVISION, /*line= */ 0, commentMessage, /*unresolved= */ true);
+ CommentsUtil.addComments(gApi, changeId, revId, comment);
+
+ Map<String, List<CommentInfo>> results = getPublishedComments(changeId, revId);
+ CommentInfo oldComment = Iterables.getOnlyElement(results.get(COMMIT_MSG));
+
+ DeleteCommentInput input = new DeleteCommentInput("reason");
+ gApi.changes().id(changeId).revision(revId).comment(oldComment.id).delete(input);
+ CommentInfo updatedComment =
+ Iterables.getOnlyElement(getPublishedComments(changeId, revId).get(COMMIT_MSG));
+
+ assertThat(updatedComment.message).doesNotContain(commentMessage);
+ assertThat(updatedComment.unresolved).isFalse();
+ }
+
+ @Test
public void patchsetLevelCommentEmailNotification() throws Exception {
PushOneCommit.Result result = createChange();
String changeId = result.getChangeId();
@@ -640,7 +667,7 @@
public void putDraft() throws Exception {
for (Integer line : lines) {
PushOneCommit.Result r = createChange();
- Timestamp origLastUpdated = r.getChange().change().getLastUpdatedOn();
+ Instant origLastUpdated = r.getChange().change().getLastUpdatedOn();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
String path = "file1";
@@ -887,7 +914,7 @@
public void deleteDraft() throws Exception {
for (Integer line : lines) {
PushOneCommit.Result r = createChange();
- Timestamp origLastUpdated = r.getChange().change().getLastUpdatedOn();
+ Instant origLastUpdated = r.getChange().change().getLastUpdatedOn();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
DraftInput draft = CommentsUtil.newDraft("file1", Side.REVISION, line, "comment 1");
@@ -903,7 +930,7 @@
@Test
public void insertCommentsWithHistoricTimestamp() throws Exception {
- Timestamp timestamp = new Timestamp(0);
+ Instant timestamp = Instant.EPOCH;
for (Integer line : lines) {
String file = "file";
String contents = "contents " + line;
@@ -912,11 +939,11 @@
PushOneCommit.Result r = push.to("refs/for/master");
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
- Timestamp origLastUpdated = r.getChange().change().getLastUpdatedOn();
+ Instant origLastUpdated = r.getChange().change().getLastUpdatedOn();
ReviewInput input = new ReviewInput();
CommentInput comment = CommentsUtil.newComment(file, Side.REVISION, line, "comment 1", false);
- comment.updated = timestamp;
+ comment.setUpdated(timestamp);
input.comments = new HashMap<>();
input.comments.put(comment.path, Lists.newArrayList(comment));
ChangeResource changeRsrc =
@@ -1853,6 +1880,72 @@
assertThat(exception.getMessage()).contains(String.format("%s not found", comment.inReplyTo));
}
+ @Test
+ public void commentsOnRootCommitsAreIncludedInEmails() throws Exception {
+ // Create a change in a new branch, making the patch-set commit a root commit.
+ ChangeInfo changeInfo = createChangeInNewBranch("newBranch");
+ Change.Id changeId = Change.Id.tryParse(Integer.toString(changeInfo._number)).get();
+
+ // Add a file.
+ gApi.changes().id(changeId.get()).edit().modifyFile("f1.txt", RawInputUtil.create("content"));
+ gApi.changes().id(changeId.get()).edit().publish();
+ email.clear();
+
+ ReviewerInput reviewerInput = new ReviewerInput();
+ reviewerInput.reviewer = admin.email();
+ gApi.changes().id(changeId.get()).addReviewer(reviewerInput);
+ changeInfo = gApi.changes().id(changeId.get()).get();
+ assertThat(email.getMessages()).hasSize(1);
+ Message message = email.getMessages().get(0);
+ assertThat(message.body()).contains("f1.txt");
+ email.clear();
+
+ // Send a comment. Make sure the email that is sent includes the comment text.
+ CommentInput c1 =
+ CommentsUtil.newComment(
+ "f1.txt",
+ Side.REVISION,
+ /* line= */ 1,
+ /* message= */ "Comment text",
+ /* unresolved= */ false);
+ CommentsUtil.addComments(gApi, changeId.toString(), changeInfo.currentRevision, c1);
+ assertThat(email.getMessages()).hasSize(1);
+ Message commentMessage = email.getMessages().get(0);
+ assertThat(commentMessage.body())
+ .contains("Patch Set 2:\n" + "\n" + "(1 comment)\n" + "\n" + "File f1.txt:");
+ assertThat(commentMessage.body()).contains("PS2, Line 1: content\n" + "Comment text");
+ }
+
+ @Test
+ public void commentsOnDeletedFileIsIncludedInEmails() throws Exception {
+ // Create a change with a file.
+ createChange("subject", "f1.txt", "content");
+
+ // Stack a second change that deletes the file.
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ gApi.changes().id(changeId).edit().deleteFile("f1.txt");
+ gApi.changes().id(changeId).edit().publish();
+ String currentRevision = gApi.changes().id(changeId).get().currentRevision;
+
+ // Add a comment on the deleted file on the parent side.
+ email.clear();
+ CommentInput commentInput =
+ CommentsUtil.newComment(
+ "f1.txt",
+ Side.PARENT,
+ /* line= */ 1,
+ /* message= */ "Comment text",
+ /* unresolved= */ false);
+ CommentsUtil.addComments(gApi, changeId, currentRevision, commentInput);
+
+ // Assert email contains the comment text.
+ assertThat(email.getMessages()).hasSize(1);
+ Message commentMessage = email.getMessages().get(0);
+ assertThat(commentMessage.body()).contains("Patch Set 2:\n\n(1 comment)\n\nFile f1.txt:");
+ assertThat(commentMessage.body()).contains("PS2, Line 1: content\nComment text");
+ }
+
private List<CommentInfo> getRevisionComments(String changeId, String revId) throws Exception {
return getPublishedComments(changeId, revId).values().stream()
.flatMap(List::stream)
@@ -2017,4 +2110,13 @@
reviewInput.draftIdsToPublish = draftIdsToPublish;
return reviewInput;
}
+
+ private ChangeInfo createChangeInNewBranch(String branchName) throws Exception {
+ ChangeInput in = new ChangeInput();
+ in.project = project.get();
+ in.branch = branchName;
+ in.newBranch = true;
+ in.subject = "New changes";
+ return gApi.changes().create(in).get();
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index 9d821b7..5b6da36 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -732,7 +732,7 @@
}
private BatchUpdate newUpdate(Account.Id owner) {
- return batchUpdateFactory.create(project, userFactory.create(owner), TimeUtil.nowTs());
+ return batchUpdateFactory.create(project, userFactory.create(owner), TimeUtil.now());
}
private ChangeNotes insertChange() throws Exception {
@@ -825,10 +825,14 @@
assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
private void addNoteDbCommit(Change.Id id, String commitMessage) throws Exception {
PersonIdent committer = serverIdent.get();
PersonIdent author =
- noteUtil.newAccountIdIdent(getAccount(admin.id()).id(), committer.getWhen(), committer);
+ noteUtil.newAccountIdIdent(
+ getAccount(admin.id()).id(), committer.getWhen().toInstant(), committer);
serverSideTestRepo
.branch(RefNames.changeMetaRef(id))
.commit()
diff --git a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index e778a5c..fd3ac7f 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -666,7 +666,7 @@
}
private void clearGroups(PatchSet.Id psId) throws Exception {
- try (BatchUpdate bu = batchUpdateFactory.create(project, user(user), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = batchUpdateFactory.create(project, user(user), TimeUtil.now())) {
bu.addOp(
psId.changeId(),
new BatchUpdateOp() {
diff --git a/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java b/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
index a97fb49..7e0bce9 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/SubmittedTogetherIT.java
@@ -130,6 +130,8 @@
} else {
assertSubmittedTogether(id1);
assertSubmittedTogether(id2);
+ assertSubmittedTogetherWithTopicClosure(id1, id2, id1);
+ assertSubmittedTogetherWithTopicClosure(id2, id2, id1);
}
}
@@ -152,6 +154,8 @@
} else {
assertSubmittedTogether(id1);
assertSubmittedTogether(id2);
+ assertSubmittedTogetherWithTopicClosure(id1, id2, id1);
+ assertSubmittedTogetherWithTopicClosure(id2, id2, id1);
}
}
@@ -180,6 +184,9 @@
assertSubmittedTogether(id1);
assertSubmittedTogether(id2);
assertSubmittedTogether(id3, id3, id2);
+ assertSubmittedTogetherWithTopicClosure(id1, id2, id1);
+ assertSubmittedTogetherWithTopicClosure(id2, id2, id1);
+ assertSubmittedTogetherWithTopicClosure(id3, id3, id2, id1);
}
}
@@ -227,6 +234,13 @@
assertSubmittedTogether(id4, id4, id3, id2);
assertSubmittedTogether(id5);
assertSubmittedTogether(id6, id6, id5);
+
+ assertSubmittedTogetherWithTopicClosure(id1, id6, id5, id3, id2, id1);
+ assertSubmittedTogetherWithTopicClosure(id2, id6, id5, id2);
+ assertSubmittedTogetherWithTopicClosure(id3, id6, id5, id3, id2, id1);
+ assertSubmittedTogetherWithTopicClosure(id4, id6, id5, id4, id3, id2, id1);
+ assertSubmittedTogetherWithTopicClosure(id5);
+ assertSubmittedTogetherWithTopicClosure(id6, id6, id5, id2);
}
}
diff --git a/javatests/com/google/gerrit/acceptance/server/git/CodeReviewCommitTest.java b/javatests/com/google/gerrit/acceptance/server/git/CodeReviewCommitTest.java
new file mode 100644
index 0000000..b5e1b07
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/git/CodeReviewCommitTest.java
@@ -0,0 +1,74 @@
+// 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.git;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.testing.InMemoryRepositoryManager;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Test;
+
+/** Tests for {@link CodeReviewCommit}. */
+public class CodeReviewCommitTest {
+
+ @Test
+ public void checkSerializable_withStatusMessage() throws Exception {
+ CodeReviewCommit commit = new CodeReviewCommit(createCommit());
+ commit.setStatusMessage("Status");
+ CodeReviewCommit deserializedCommit = serializeAndReadBack(commit);
+ assertThat(deserializedCommit).isEqualTo(commit);
+ assertThat(deserializedCommit.getStatusMessage().get()).isEqualTo("Status");
+ }
+
+ @Test
+ public void checkSerializable_emptyStatusMessage() throws Exception {
+ CodeReviewCommit commit = new CodeReviewCommit(createCommit());
+ CodeReviewCommit deserializedCommit = serializeAndReadBack(commit);
+ assertThat(deserializedCommit).isEqualTo(commit);
+ assertThat(deserializedCommit.getStatusMessage().isPresent()).isFalse();
+ }
+
+ private CodeReviewCommit serializeAndReadBack(CodeReviewCommit codeReviewCommit)
+ throws Exception {
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ObjectOutputStream out = new ObjectOutputStream(bos)) {
+ out.writeObject(codeReviewCommit);
+ out.flush();
+ try (ByteArrayInputStream fileIn = new ByteArrayInputStream(bos.toByteArray());
+ ObjectInputStream in = new ObjectInputStream(fileIn); ) {
+ return (CodeReviewCommit) in.readObject();
+ }
+ }
+ }
+
+ private ObjectId createCommit() throws Exception {
+ InMemoryRepositoryManager repoManager = new InMemoryRepositoryManager();
+ Project.NameKey project = Project.nameKey("test");
+ try (Repository repo = repoManager.createRepository(project);
+ TestRepository<Repository> tr = new TestRepository<>(repo)) {
+ PersonIdent ident = new PersonIdent(new PersonIdent("Test Ident", "test@test.com"));
+ return tr.commit().author(ident).committer(ident).message("Test commit").create();
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java b/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java
index 1a01184..13e2f24 100644
--- a/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/git/receive/ReceiveCommitsCommentValidationIT.java
@@ -95,10 +95,7 @@
PushOneCommit.Result result = createChange();
String changeId = result.getChangeId();
String revId = result.getCommit().getName();
- when(mockCommentValidator.validateComments(
- CommentValidationContext.create(
- result.getChange().getId().get(), result.getChange().project().get()),
- ImmutableList.of(COMMENT_FOR_VALIDATION)))
+ when(mockCommentValidator.validateComments(captureCtx.capture(), capture.capture()))
.thenReturn(ImmutableList.of());
DraftInput comment = testCommentHelper.newDraft(COMMENT_TEXT);
testCommentHelper.addDraft(changeId, revId, comment);
@@ -107,6 +104,12 @@
amendResult.assertOkStatus();
amendResult.assertNotMessage("Comment validation failure:");
assertThat(testCommentHelper.getPublishedComments(result.getChangeId())).hasSize(1);
+
+ assertThat(captureCtx.getAllValues()).hasSize(1);
+ assertThat(captureCtx.getValue().getProject()).isEqualTo(result.getChange().project().get());
+ assertThat(captureCtx.getValue().getChangeId()).isEqualTo(result.getChange().getId().get());
+ assertThat(captureCtx.getValue().getRefName()).isEqualTo("refs/heads/master");
+ assertThat(capture.getValue()).containsExactly(COMMENT_FOR_VALIDATION);
}
@Test
@@ -182,7 +185,9 @@
String revId = result.getCommit().getName();
when(mockCommentValidator.validateComments(
CommentValidationContext.create(
- result.getChange().getId().get(), result.getChange().project().get()),
+ result.getChange().getId().get(),
+ result.getChange().project().get(),
+ result.getChange().change().getDest().branch()),
ImmutableList.of(COMMENT_FOR_VALIDATION)))
.thenReturn(ImmutableList.of(COMMENT_FOR_VALIDATION.failValidation("Oh no!")));
DraftInput comment = testCommentHelper.newDraft(COMMENT_TEXT);
@@ -215,6 +220,7 @@
assertThat(captureCtx.getValue().getProject()).isEqualTo(result.getChange().project().get());
assertThat(captureCtx.getValue().getChangeId()).isEqualTo(result.getChange().getId().get());
+ assertThat(captureCtx.getValue().getRefName()).isEqualTo("refs/heads/master");
assertThat(capture.getAllValues().get(0))
.containsExactly(
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
index d6fcccc..4e490a7 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
@@ -30,11 +30,10 @@
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.testing.FakeEmailSender;
import com.google.inject.Inject;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Collection;
-import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -100,7 +99,7 @@
expectedHeaders.put("Gerrit-MessageType", "comment");
expectedHeaders.put("Gerrit-Commit", newChange.getCommit().getId().name());
expectedHeaders.put("Gerrit-ChangeURL", changeURL);
- expectedHeaders.put("Gerrit-Comment-Date", Iterables.getLast(result).date);
+ expectedHeaders.put("Gerrit-Comment-Date", Iterables.getLast(result).date.toInstant());
assertHeaders(message.headers(), expectedHeaders);
@@ -116,14 +115,14 @@
if (entry.getValue() instanceof String) {
assertThat(have)
.containsEntry("X-" + entry.getKey(), new StringEmailHeader((String) entry.getValue()));
- } else if (entry.getValue() instanceof Date) {
+ } else if (entry.getValue() instanceof Instant) {
assertThat(have)
- .containsEntry("X-" + entry.getKey(), new EmailHeader.Date((Date) entry.getValue()));
+ .containsEntry("X-" + entry.getKey(), new EmailHeader.Date((Instant) entry.getValue()));
} else {
throw new Exception(
"Object has unsupported type: "
+ entry.getValue().getClass().getName()
- + " must be java.util.Date or java.lang.String for key "
+ + " must be java.time.Instant or java.lang.String for key "
+ entry.getKey());
}
}
@@ -133,19 +132,18 @@
for (Map.Entry<String, Object> entry : want.entrySet()) {
if (entry.getValue() instanceof String) {
assertThat(body).contains(entry.getKey() + ": " + entry.getValue());
- } else if (entry.getValue() instanceof Timestamp) {
+ } else if (entry.getValue() instanceof Instant) {
assertThat(body)
.contains(
entry.getKey()
+ ": "
+ MailProcessingUtil.rfcDateformatter.format(
- ZonedDateTime.ofInstant(
- ((Timestamp) entry.getValue()).toInstant(), ZoneId.of("UTC"))));
+ ZonedDateTime.ofInstant((Instant) entry.getValue(), ZoneId.of("UTC"))));
} else {
throw new Exception(
"Object has unsupported type: "
+ entry.getValue().getClass().getName()
- + " must be java.util.Date or java.lang.String for key "
+ + " must be java.time.Instant or java.lang.String for key "
+ entry.getKey());
}
}
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index d4000b3..2bccc87 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -436,7 +436,7 @@
String txt = newPlaintextBody(getChangeUrl(changeInfo) + "/1", COMMENT_TEXT, null, null);
b.textContent(txt + textFooterForChange(changeInfo._number, ts));
- Collection<CommentInfo> commentsBefore = testCommentHelper.getPublishedComments(changeId);
+ List<CommentInfo> commentsBefore = testCommentHelper.getPublishedComments(changeId);
mailProcessor.process(b.build());
assertThat(testCommentHelper.getPublishedComments(changeId)).isEqualTo(commentsBefore);
@@ -445,7 +445,7 @@
assertThat(message.body()).contains("rejected one or more comments");
// ensure the message header contains a valid message id.
- assertThat(((StringEmailHeader) (message.headers().get("Message-ID"))).getString())
+ assertThat(((StringEmailHeader) message.headers().get("Message-ID")).getString())
.containsMatch("<someid-REJECTION-HTML@" + new URL(canonicalWebUrl.get()).getHost() + ">");
}
@@ -465,7 +465,7 @@
String txt = newPlaintextBody(getChangeUrl(changeInfo) + "/1", null, COMMENT_TEXT, null);
b.textContent(txt + textFooterForChange(changeInfo._number, ts));
- Collection<CommentInfo> commentsBefore = testCommentHelper.getPublishedComments(changeId);
+ List<CommentInfo> commentsBefore = testCommentHelper.getPublishedComments(changeId);
mailProcessor.process(b.build());
assertThat(testCommentHelper.getPublishedComments(changeId)).isEqualTo(commentsBefore);
@@ -490,7 +490,7 @@
String txt = newPlaintextBody(getChangeUrl(changeInfo) + "/1", null, null, COMMENT_TEXT);
b.textContent(txt + textFooterForChange(changeInfo._number, ts));
- Collection<CommentInfo> commentsBefore = testCommentHelper.getPublishedComments(changeId);
+ List<CommentInfo> commentsBefore = testCommentHelper.getPublishedComments(changeId);
mailProcessor.process(b.build());
assertThat(testCommentHelper.getPublishedComments(changeId)).isEqualTo(commentsBefore);
@@ -576,7 +576,7 @@
null);
mailMessage.textContent(txt + textFooterForChange(changeInfo._number, ts));
- Collection<CommentInfo> commentsBefore = testCommentHelper.getPublishedComments(changeId);
+ List<CommentInfo> commentsBefore = testCommentHelper.getPublishedComments(changeId);
mailProcessor.process(mailMessage.build());
assertThat(testCommentHelper.getPublishedComments(changeId)).isEqualTo(commentsBefore);
@@ -596,7 +596,7 @@
CommentForValidation.CommentSource.HUMAN, type, COMMENT_TEXT, COMMENT_TEXT.length());
when(mockCommentValidator.validateComments(
- CommentValidationContext.create(failChange, failProject),
+ CommentValidationContext.create(failChange, failProject, "refs/heads/master"),
ImmutableList.of(commentForValidation)))
.thenReturn(ImmutableList.of(commentForValidation.failValidation("Oh no!")));
}
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java b/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java
index 628b90c..65b1d4f 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/NotificationMailFormatIT.java
@@ -18,12 +18,17 @@
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailFormat;
+import com.google.gerrit.extensions.client.ProjectWatchInfo;
import com.google.gerrit.testing.FakeEmailSender;
import com.google.inject.Inject;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
import org.junit.Test;
public class NotificationMailFormatIT extends AbstractDaemonTest {
@@ -56,6 +61,33 @@
}
@Test
+ public void bccUserIsNotAddedToReplyTo() throws Exception {
+ TestAccount bccUser = accountCreator.user2();
+
+ List<ProjectWatchInfo> projectsToWatch = new ArrayList<>();
+ ProjectWatchInfo pwi = new ProjectWatchInfo();
+ pwi.project = project.get();
+ pwi.notifyAllComments = true;
+ projectsToWatch.add(pwi);
+
+ gApi.accounts().id(bccUser.id().get()).setWatchedProjects(projectsToWatch);
+
+ PushOneCommit.Result r = createChange();
+
+ requestScopeOperations.setApiUser(user.id());
+ gApi.changes().id(r.getChangeId()).current().review(ReviewInput.recommend());
+
+ assertThat(sender.getMessages()).hasSize(1);
+ FakeEmailSender.Message m = sender.getMessages().get(0);
+ assertMailReplyTo(m, admin.email());
+ assertMailReplyTo(m, user.email());
+ assertMailNotReplyTo(m, bccUser.email());
+
+ assertThat(m.rcpt().stream().map(a -> a.email()).collect(Collectors.toSet()))
+ .contains(bccUser.email());
+ }
+
+ @Test
public void userReceivesHtmlAndPlaintextEmail() throws Exception {
// Create change as admin and review as user
PushOneCommit.Result r = createChange();
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
index 3c066a3..ab5e1d8 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
@@ -274,7 +274,7 @@
}
private BatchUpdate newBatchUpdate(BatchUpdate.Factory buf) {
- return buf.create(project, identifiedUserFactory.create(user.id()), TimeUtil.nowTs());
+ return buf.create(project, identifiedUserFactory.create(user.id()), TimeUtil.now());
}
private Optional<ObjectId> getRef(String name) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index ba86976..d911512 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -17,6 +17,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
@@ -46,16 +47,21 @@
@Test
public void newPatchSetsNotifyConfig() throws Exception {
- Address addr = Address.create("Watcher", "watcher@example.com");
- NotifyConfig.Builder nc = NotifyConfig.builder();
- nc.addAddress(addr);
- nc.setName("new-patch-set");
- nc.setHeader(NotifyConfig.Header.CC);
- nc.setNotify(EnumSet.of(NotifyType.NEW_PATCHSETS));
- nc.setFilter("message:sekret");
-
+ ImmutableList<String> messageFilters =
+ ImmutableList.of("message:subject-with-tokens", "message:subject-with-tokens=secret");
+ ImmutableList.Builder<Address> watchers = ImmutableList.builder();
try (ProjectConfigUpdate u = updateProject(project)) {
- u.getConfig().putNotifyConfig("watch", nc.build());
+ for (int i = 0; i < messageFilters.size(); i++) {
+ Address addr = Address.create("Watcher#" + i, String.format("watcher-%s@example.com", i));
+ watchers.add(addr);
+ NotifyConfig.Builder nc = NotifyConfig.builder();
+ nc.addAddress(addr);
+ nc.setName("new-patch-set" + i);
+ nc.setHeader(NotifyConfig.Header.CC);
+ nc.setNotify(EnumSet.of(NotifyType.NEW_PATCHSETS));
+ nc.setFilter(messageFilters.get(i));
+ u.getConfig().putNotifyConfig("watch" + i, nc.build());
+ }
u.save();
}
@@ -67,7 +73,13 @@
r =
pushFactory
- .create(admin.newIdent(), testRepo, "super sekret subject", "a", "a2", r.getChangeId())
+ .create(
+ admin.newIdent(),
+ testRepo,
+ "super sekret subject\n\nsubject-with-tokens=secret subject",
+ "a",
+ "a2",
+ r.getChangeId())
.to("refs/for/master");
r.assertOkStatus();
@@ -80,7 +92,7 @@
List<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
- assertThat(m.rcpt()).containsExactly(addr);
+ assertThat(m.rcpt()).containsExactlyElementsIn(watchers.build());
assertThat(m.body()).contains("Change subject: super sekret subject\n");
assertThat(m.body()).contains("Gerrit-PatchSet: 2\n");
}
diff --git a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
index 6e19c39..61e204f 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsEvaluatorIT.java
@@ -21,10 +21,13 @@
import com.google.common.collect.MoreCollectors;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementExpression;
@@ -32,12 +35,14 @@
import com.google.gerrit.entities.SubmitRequirementExpressionResult.Status;
import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.project.SubmitRequirementsEvaluator;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.inject.Inject;
import com.google.inject.Provider;
+import java.util.Map;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
@@ -47,6 +52,7 @@
@Inject SubmitRequirementsEvaluator evaluator;
@Inject private ProjectOperations projectOperations;
@Inject private Provider<InternalChangeQuery> changeQueryProvider;
+ @Inject private ExtensionRegistry extensionRegistry;
private ChangeData changeData;
private String changeId;
@@ -108,6 +114,93 @@
}
@Test
+ public void globalSubmitRequirementEvaluated() throws Exception {
+ SubmitRequirement globalSubmitRequirement =
+ createSubmitRequirement(
+ /*name=*/ "global-config-requirement",
+ /* applicabilityExpr= */ "project:" + project.get(),
+ /*submittabilityExpr= */ "is:true",
+ /* overrideExpr= */ "", /*allowOverrideInChildProjects*/
+ false);
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(globalSubmitRequirement)) {
+ SubmitRequirement projectSubmitRequirement =
+ createSubmitRequirement(
+ /*name=*/ "project-config-requirement",
+ /* applicabilityExpr= */ "project:" + project.get(),
+ /*submittabilityExpr= */ "is:true",
+ /* overrideExpr= */ "", /*allowOverrideInChildProjects*/
+ false);
+ configSubmitRequirement(project, projectSubmitRequirement);
+ Map<SubmitRequirement, SubmitRequirementResult> results =
+ evaluator.evaluateAllRequirements(changeData, /* includeLegacy= */ false);
+ assertThat(results).hasSize(2);
+ assertThat(results.get(globalSubmitRequirement).status())
+ .isEqualTo(SubmitRequirementResult.Status.SATISFIED);
+ assertThat(results.get(projectSubmitRequirement).status())
+ .isEqualTo(SubmitRequirementResult.Status.SATISFIED);
+ }
+ }
+
+ @Test
+ public void
+ globalSubmitRequirement_duplicateInProjectConfig_overrideAllowed_projectResultReturned()
+ throws Exception {
+ SubmitRequirement globalSubmitRequirement =
+ createSubmitRequirement(
+ /*name=*/ "config-requirement",
+ /* applicabilityExpr= */ "project:" + project.get(),
+ /*submittabilityExpr= */ "is:true",
+ /* overrideExpr= */ "", /*allowOverrideInChildProjects*/
+ true);
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(globalSubmitRequirement)) {
+ SubmitRequirement projectSubmitRequirement =
+ createSubmitRequirement(
+ /*name=*/ "config-requirement",
+ /* applicabilityExpr= */ "project:" + project.get(),
+ /*submittabilityExpr= */ "is:true",
+ /* overrideExpr= */ "", /*allowOverrideInChildProjects*/
+ false);
+ configSubmitRequirement(project, projectSubmitRequirement);
+ Map<SubmitRequirement, SubmitRequirementResult> results =
+ evaluator.evaluateAllRequirements(changeData, /* includeLegacy= */ false);
+ assertThat(results).hasSize(1);
+ assertThat(results.get(projectSubmitRequirement).status())
+ .isEqualTo(SubmitRequirementResult.Status.SATISFIED);
+ }
+ }
+
+ @Test
+ public void
+ globalSubmitRequirement_duplicateInProjectConfig_overrideNotAllowedAllowed_globalResultReturned()
+ throws Exception {
+ SubmitRequirement globalSubmitRequirement =
+ createSubmitRequirement(
+ /*name=*/ "config-requirement",
+ /* applicabilityExpr= */ "project:" + project.get(),
+ /*submittabilityExpr= */ "is:true",
+ /* overrideExpr= */ "", /*allowOverrideInChildProjects*/
+ false);
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(globalSubmitRequirement)) {
+ SubmitRequirement projectSubmitRequirement =
+ createSubmitRequirement(
+ /*name=*/ "config-requirement",
+ /* applicabilityExpr= */ "project:" + project.get(),
+ /*submittabilityExpr= */ "is:true",
+ /* overrideExpr= */ "", /*allowOverrideInChildProjects*/
+ false);
+ configSubmitRequirement(project, projectSubmitRequirement);
+ Map<SubmitRequirement, SubmitRequirementResult> results =
+ evaluator.evaluateAllRequirements(changeData, /* includeLegacy= */ false);
+ assertThat(results).hasSize(1);
+ assertThat(results.get(globalSubmitRequirement).status())
+ .isEqualTo(SubmitRequirementResult.Status.SATISFIED);
+ }
+ }
+
+ @Test
public void submitRequirementIsNotApplicable_whenApplicabilityExpressionIsFalse()
throws Exception {
SubmitRequirement sr =
@@ -121,6 +214,30 @@
}
@Test
+ public void submitRequirement_alwaysNotApplicable() {
+ SubmitRequirement sr =
+ createSubmitRequirement(
+ /* applicabilityExpr= */ "is:false",
+ /* submittabilityExpr= */ "is:false", // redundant
+ /* overrideExpr= */ "");
+
+ SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.NOT_APPLICABLE);
+ }
+
+ @Test
+ public void submitRequirement_alwaysApplicable() {
+ SubmitRequirement sr =
+ createSubmitRequirement(
+ /* applicabilityExpr= */ "is:true",
+ /* submittabilityExpr= */ "is:true",
+ /* overrideExpr= */ "");
+
+ SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
+ }
+
+ @Test
public void submitRequirementIsSatisfied_whenSubmittabilityExpressionIsTrue() throws Exception {
SubmitRequirement sr =
createSubmitRequirement(
@@ -138,13 +255,13 @@
SubmitRequirement sr =
createSubmitRequirement(
/* applicabilityExpr= */ "project:" + project.get(),
- /* submittabilityExpr= */ "label:\"code-review=+2\"",
+ /* submittabilityExpr= */ "label:\"Code-Review=+2\"",
/* overrideExpr= */ "");
SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.UNSATISFIED);
assertThat(result.submittabilityExpressionResult().failingAtoms())
- .containsExactly("label:\"code-review=+2\"");
+ .containsExactly("label:\"Code-Review=+2\"");
}
@Test
@@ -160,7 +277,7 @@
SubmitRequirement sr =
createSubmitRequirement(
/* applicabilityExpr= */ "project:" + project.get(),
- /* submittabilityExpr= */ "label:\"code-review=+2\"",
+ /* submittabilityExpr= */ "label:\"Code-Review=+2\"",
/* overrideExpr= */ "label:\"build-cop-override=+1\"");
SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
@@ -175,7 +292,7 @@
SubmitRequirement sr =
createSubmitRequirement(
/* applicabilityExpr= */ "invalid_field:invalid_value",
- /* submittabilityExpr= */ "label:\"code-review=+2\"",
+ /* submittabilityExpr= */ "label:\"Code-Review=+2\"",
/* overrideExpr= */ "label:\"build-cop-override=+1\"");
SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
@@ -206,7 +323,7 @@
SubmitRequirement sr =
createSubmitRequirement(
/* applicabilityExpr= */ "project:" + project.get(),
- /* submittabilityExpr= */ "label:\"code-review=+2\"",
+ /* submittabilityExpr= */ "label:\"Code-Review=+2\"",
/* overrideExpr= */ "invalid_field:invalid_value");
SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
@@ -215,6 +332,33 @@
.isEqualTo("Unsupported operator invalid_field:invalid_value");
}
+ @Test
+ public void byPureRevert() throws Exception {
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result pushResult =
+ createChange(testRepo, "refs/heads/master", "Fix a bug", "file.txt", "content", "topic");
+ changeData = pushResult.getChange();
+ changeId = pushResult.getChangeId();
+
+ SubmitRequirement sr =
+ createSubmitRequirement(
+ /* applicabilityExpr= */ "project:" + project.get(),
+ /* submittabilityExpr= */ "is:pure-revert",
+ /* overrideExpr= */ "");
+
+ SubmitRequirementResult result = evaluator.evaluateRequirement(sr, changeData);
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.UNSATISFIED);
+ approve(changeId);
+ gApi.changes().id(changeId).current().submit();
+
+ ChangeInfo changeInfo = gApi.changes().id(changeId).revert().get();
+ String revertId = Integer.toString(changeInfo._number);
+ ChangeData revertChangeData =
+ changeQueryProvider.get().byLegacyChangeId(Change.Id.tryParse(revertId).get()).get(0);
+ result = evaluator.evaluateRequirement(sr, revertChangeData);
+ assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
+ }
+
private void voteLabel(String changeId, String labelName, int score) throws RestApiException {
gApi.changes().id(changeId).current().review(new ReviewInput().label(labelName, score));
}
@@ -239,13 +383,27 @@
@Nullable String applicabilityExpr,
String submittabilityExpr,
@Nullable String overrideExpr) {
+ return createSubmitRequirement(
+ /*name= */ "sr-name",
+ applicabilityExpr,
+ submittabilityExpr,
+ overrideExpr,
+ /*allowOverrideInChildProjects=*/ false);
+ }
+
+ private SubmitRequirement createSubmitRequirement(
+ String name,
+ @Nullable String applicabilityExpr,
+ String submittabilityExpr,
+ @Nullable String overrideExpr,
+ boolean allowOverrideInChildProjects) {
return SubmitRequirement.builder()
- .setName("sr-name")
+ .setName(name)
.setDescription(Optional.of("sr-description"))
.setApplicabilityExpression(SubmitRequirementExpression.of(applicabilityExpr))
.setSubmittabilityExpression(SubmitRequirementExpression.create(submittabilityExpr))
.setOverrideExpression(SubmitRequirementExpression.of(overrideExpr))
- .setAllowOverrideInChildProjects(false)
+ .setAllowOverrideInChildProjects(allowOverrideInChildProjects)
.build();
}
}
diff --git a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsValidationIT.java b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsValidationIT.java
index 4675bc0..d8aa789 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsValidationIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/SubmitRequirementsValidationIT.java
@@ -50,7 +50,7 @@
ProjectConfig.SUBMIT_REQUIREMENT,
/* subsection= */ submitRequirementName,
/* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
- /* value= */ "label:\"code-review=+2\""));
+ /* value= */ "label:\"Code-Review=+2\""));
PushResult r = pushRefsMetaConfig();
assertOkStatus(r);
@@ -77,7 +77,7 @@
ProjectConfig.SUBMIT_REQUIREMENT,
/* subsection= */ submitRequirementName,
/* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
- /* value= */ "label:\"code-review=+2\"");
+ /* value= */ "label:\"Code-Review=+2\"");
projectConfig.setString(
ProjectConfig.SUBMIT_REQUIREMENT,
/* subsection= */ submitRequirementName,
@@ -95,6 +95,78 @@
}
@Test
+ public void parametersDirectlyInSubmitRequirementsSectionAreRejected() throws Exception {
+ fetchRefsMetaConfig();
+
+ updateProjectConfig(
+ projectConfig -> {
+ projectConfig.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ /* subsection= */ null,
+ /* name= */ ProjectConfig.KEY_SR_DESCRIPTION,
+ /* value= */ "foo bar description");
+ projectConfig.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ /* subsection= */ null,
+ /* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
+ /* value= */ "label:\"Code-Review=+2\"");
+ });
+
+ PushResult r = pushRefsMetaConfig();
+ assertErrorStatus(
+ r,
+ "Invalid project configuration",
+ String.format(
+ "project.config: Submit requirements must be defined in submit-requirement.<name>"
+ + " subsections. Setting parameters directly in the submit-requirement section is"
+ + " not allowed: [%s, %s]",
+ ProjectConfig.KEY_SR_DESCRIPTION, ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION));
+ }
+
+ @Test
+ public void unsupportedParameterDirectlyInSubmitRequirementsSectionIsRejected() throws Exception {
+ fetchRefsMetaConfig();
+
+ updateProjectConfig(
+ projectConfig ->
+ projectConfig.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ /* subsection= */ null,
+ /* name= */ "unknown",
+ /* value= */ "value"));
+
+ PushResult r = pushRefsMetaConfig();
+ assertErrorStatus(
+ r,
+ "Invalid project configuration",
+ "project.config: Submit requirements must be defined in submit-requirement.<name>"
+ + " subsections. Setting parameters directly in the submit-requirement section is"
+ + " not allowed: [unknown]");
+ }
+
+ @Test
+ public void unsupportedParameterForSubmitRequirementIsRejected() throws Exception {
+ fetchRefsMetaConfig();
+
+ String submitRequirementName = "Code-Review";
+ updateProjectConfig(
+ projectConfig ->
+ projectConfig.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ /* subsection= */ submitRequirementName,
+ /* name= */ "unknown",
+ /* value= */ "value"));
+
+ PushResult r = pushRefsMetaConfig();
+ assertErrorStatus(
+ r,
+ "Invalid project configuration",
+ String.format(
+ "project.config: Unsupported parameters for submit requirement '%s': [unknown]",
+ submitRequirementName));
+ }
+
+ @Test
public void conflictingSubmitRequirementsAreRejected() throws Exception {
fetchRefsMetaConfig();
@@ -105,12 +177,12 @@
ProjectConfig.SUBMIT_REQUIREMENT,
/* subsection= */ submitRequirementName,
/* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
- /* value= */ "label:\"code-review=+2\"");
+ /* value= */ "label:\"Code-Review=+2\"");
projectConfig.setString(
ProjectConfig.SUBMIT_REQUIREMENT,
/* subsection= */ submitRequirementName.toLowerCase(Locale.US),
/* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
- /* value= */ "label:\"code-review=+2\"");
+ /* value= */ "label:\"Code-Review=+2\"");
});
PushResult r = pushRefsMetaConfig();
@@ -132,7 +204,7 @@
ProjectConfig.SUBMIT_REQUIREMENT,
/* subsection= */ submitRequirementName,
/* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
- /* value= */ "label:\"code-review=+2\""));
+ /* value= */ "label:\"Code-Review=+2\""));
PushResult r = pushRefsMetaConfig();
assertOkStatus(r);
@@ -142,7 +214,7 @@
ProjectConfig.SUBMIT_REQUIREMENT,
/* subsection= */ submitRequirementName.toLowerCase(Locale.US),
/* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
- /* value= */ "label:\"code-review=+2\""));
+ /* value= */ "label:\"Code-Review=+2\""));
r = pushRefsMetaConfig();
assertErrorStatus(
r,
@@ -170,8 +242,139 @@
r,
"Invalid project configuration",
String.format(
- "project.config: Submit requirement '%s' does not define a submittability expression.",
- submitRequirementName));
+ "project.config: Setting a submittability expression for submit requirement '%s' is"
+ + " required: Missing %s.%s.%s",
+ submitRequirementName,
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ submitRequirementName,
+ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION));
+ }
+
+ @Test
+ public void submitRequirementWithInvalidSubmittabilityExpressionIsRejected() throws Exception {
+ fetchRefsMetaConfig();
+
+ String submitRequirementName = "Code-Review";
+ String invalidExpression = "invalid_field:invalid_value";
+ updateProjectConfig(
+ projectConfig ->
+ projectConfig.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ /* subsection= */ submitRequirementName,
+ /* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
+ /* value= */ invalidExpression));
+
+ PushResult r = pushRefsMetaConfig();
+ assertErrorStatus(
+ r,
+ "Invalid project configuration",
+ String.format(
+ "project.config: Expression '%s' of submit requirement '%s' (parameter %s.%s.%s) is"
+ + " invalid: Unsupported operator %s",
+ invalidExpression,
+ submitRequirementName,
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ submitRequirementName,
+ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
+ invalidExpression));
+ }
+
+ @Test
+ public void submitRequirementWithInvalidApplicabilityExpressionIsRejected() throws Exception {
+ fetchRefsMetaConfig();
+
+ String submitRequirementName = "Code-Review";
+ String invalidExpression = "invalid_field:invalid_value";
+ updateProjectConfig(
+ projectConfig -> {
+ projectConfig.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ /* subsection= */ submitRequirementName,
+ /* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
+ /* value= */ "label:\"Code-Review=+2\"");
+ projectConfig.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ /* subsection= */ submitRequirementName,
+ /* name= */ ProjectConfig.KEY_SR_APPLICABILITY_EXPRESSION,
+ /* value= */ invalidExpression);
+ });
+
+ PushResult r = pushRefsMetaConfig();
+ assertErrorStatus(
+ r,
+ "Invalid project configuration",
+ String.format(
+ "project.config: Expression '%s' of submit requirement '%s' (parameter %s.%s.%s) is"
+ + " invalid: Unsupported operator %s",
+ invalidExpression,
+ submitRequirementName,
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ submitRequirementName,
+ ProjectConfig.KEY_SR_APPLICABILITY_EXPRESSION,
+ invalidExpression));
+ }
+
+ @Test
+ public void submitRequirementWithInvalidOverrideExpressionIsRejected() throws Exception {
+ fetchRefsMetaConfig();
+
+ String submitRequirementName = "Code-Review";
+ String invalidExpression = "invalid_field:invalid_value";
+ updateProjectConfig(
+ projectConfig -> {
+ projectConfig.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ /* subsection= */ submitRequirementName,
+ /* name= */ ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
+ /* value= */ "label:\"Code-Review=+2\"");
+ projectConfig.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ /* subsection= */ submitRequirementName,
+ /* name= */ ProjectConfig.KEY_SR_OVERRIDE_EXPRESSION,
+ /* value= */ invalidExpression);
+ });
+
+ PushResult r = pushRefsMetaConfig();
+ assertErrorStatus(
+ r,
+ "Invalid project configuration",
+ String.format(
+ "project.config: Expression '%s' of submit requirement '%s' (parameter %s.%s.%s) is"
+ + " invalid: Unsupported operator %s",
+ invalidExpression,
+ submitRequirementName,
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ submitRequirementName,
+ ProjectConfig.KEY_SR_OVERRIDE_EXPRESSION,
+ invalidExpression));
+ }
+
+ @Test
+ public void submitRequirementWithInvalidAllowOverrideInChildProjectsIsRejected()
+ throws Exception {
+ fetchRefsMetaConfig();
+
+ String submitRequirementName = "Code-Review";
+ String invalidValue = "invalid";
+ updateProjectConfig(
+ projectConfig ->
+ projectConfig.setString(
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ /* subsection= */ submitRequirementName,
+ /* name= */ ProjectConfig.KEY_SR_OVERRIDE_IN_CHILD_PROJECTS,
+ /* value= */ invalidValue));
+
+ PushResult r = pushRefsMetaConfig();
+ assertErrorStatus(
+ r,
+ "Invalid project configuration",
+ String.format(
+ "project.config: Invalid value %s.%s.%s for submit requirement '%s': %s",
+ ProjectConfig.SUBMIT_REQUIREMENT,
+ submitRequirementName,
+ ProjectConfig.KEY_SR_OVERRIDE_IN_CHILD_PROJECTS,
+ submitRequirementName,
+ invalidValue));
}
private void fetchRefsMetaConfig() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java b/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java
index 537c7d8..925c855 100644
--- a/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java
@@ -35,7 +35,9 @@
import com.google.gerrit.server.query.approval.ApprovalContext;
import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
import com.google.inject.Inject;
-import java.util.Date;
+import java.time.Instant;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.Test;
public class ApprovalQueryIT extends AbstractDaemonTest {
@@ -250,7 +252,7 @@
}
private ApprovalContext contextForCodeReviewLabel(
- int value, PatchSet.Id psId, Account.Id approver) {
+ int value, PatchSet.Id psId, Account.Id approver) throws Exception {
ChangeNotes changeNotes = changeNotesFactory.create(project, psId.changeId());
PatchSet.Id newPsId = PatchSet.id(psId.changeId(), psId.get() + 1);
ChangeKind changeKind =
@@ -259,11 +261,19 @@
PatchSetApproval approval =
PatchSetApproval.builder()
.postSubmit(false)
- .granted(new Date())
+ .granted(Instant.now())
.key(PatchSetApproval.key(psId, approver, LabelId.create("Code-Review")))
.value(value)
.build();
- return ApprovalContext.create(
- changeNotes, approval, changeNotes.getPatchSets().get(newPsId), changeKind);
+ try (Repository repo = repoManager.openRepository(project);
+ RevWalk rw = new RevWalk(repo.newObjectReader())) {
+ return ApprovalContext.create(
+ changeNotes,
+ approval,
+ changeNotes.getPatchSets().get(newPsId),
+ changeKind,
+ rw,
+ repo.getConfig());
+ }
}
}
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java b/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
index 0585f74..3ba7829 100644
--- a/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
@@ -41,9 +41,6 @@
*/
@NoHttpd
public class RulesIT extends AbstractDaemonTest {
- private static final String RULE_TEMPLATE =
- "submit_rule(submit(W)) :- \n%s,\nW = label('OK', ok(user(1000000))).";
-
@Inject private ProjectOperations projectOperations;
@Inject private SubmitRuleEvaluator.Factory evaluatorFactory;
@@ -164,6 +161,12 @@
assertThat(statusForRuleRenamedFile()).isEqualTo(SubmitRecord.Status.OK);
}
+ @Test
+ public void typeError() throws Exception {
+ modifySubmitRules("user(1000000)."); // the trailing '.' triggers a type error
+ assertThat(statusForRuleAddFile("foo")).isEqualTo(SubmitRecord.Status.RULE_ERROR);
+ }
+
private SubmitRecord.Status statusForRule() throws Exception {
String oldHead = projectOperations.project(project).getHead("master").name();
PushOneCommit.Result result =
@@ -249,7 +252,9 @@
}
private void modifySubmitRules(String ruleTested) throws Exception {
- String newContent = String.format(RULE_TEMPLATE, ruleTested);
+ String newContent =
+ String.format(
+ "submit_rule(submit(W)) :- \n%s,\nW = label('OK', ok(user(1000000))).", ruleTested);
try (Repository repo = repoManager.openRepository(project);
TestRepository<Repository> testRepo = new TestRepository<>(repo)) {
diff --git a/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java b/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java
index 3b38bad..18c4952 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/AbstractIndexTests.java
@@ -29,7 +29,6 @@
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
-import com.google.inject.Injector;
import java.util.List;
import org.junit.Test;
@@ -38,15 +37,12 @@
public abstract class AbstractIndexTests extends AbstractDaemonTest {
@Inject private ExtensionRegistry extensionRegistry;
- public void configureIndex(Injector injector) {}
-
@Test
@GerritConfig(name = "index.autoReindexIfStale", value = "false")
public void indexChange() throws Exception {
ChangeIndexedCounter changeIndexedCounter = new ChangeIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(changeIndexedCounter)) {
- configureIndex(server.getTestInjector());
PushOneCommit.Result change = createChange("first change", "test1.txt", "test1");
String changeId = change.getChangeId();
@@ -76,7 +72,6 @@
ChangeIndexedCounter changeIndexedCounter = new ChangeIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(changeIndexedCounter)) {
- configureIndex(server.getTestInjector());
PushOneCommit.Result change = createChange("first change", "test1.txt", "test1");
String changeId = change.getChangeId();
diff --git a/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java b/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
index cf316c7..72498c0 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
@@ -17,6 +17,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
@@ -37,7 +38,6 @@
@NoHttpd
@UseSsh
public class QueryIT extends AbstractDaemonTest {
-
private static Gson gson = new Gson();
@Test
@@ -313,10 +313,10 @@
}
private static List<ChangeAttribute> getChanges(String rawResponse) {
- String[] lines = rawResponse.split("\\n");
- List<ChangeAttribute> changes = new ArrayList<>(lines.length - 1);
- for (int i = 0; i < lines.length - 1; i++) {
- changes.add(gson.fromJson(lines[i], ChangeAttribute.class));
+ List<String> lines = Splitter.on("\n").omitEmptyStrings().splitToList(rawResponse);
+ List<ChangeAttribute> changes = new ArrayList<>(lines.size() - 1);
+ for (int i = 0; i < lines.size() - 1; i++) {
+ changes.add(gson.fromJson(lines.get(i), ChangeAttribute.class));
}
return changes;
}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java b/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java
index 58c2517..2de52b2 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java
@@ -16,6 +16,7 @@
import static com.google.common.truth.Truth.assertThat;
+import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
@@ -50,7 +51,8 @@
@Test
public void byCommitHash() throws Exception {
- String id = change.getCommit().getId().toString().split("\\s+")[1];
+ String id =
+ Splitter.onPattern("\\s+").splitToList(change.getCommit().getId().toString()).get(1);
addReviewer(id);
removeReviewer(id);
}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
index 7224e19..ac8a200 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
@@ -206,22 +206,22 @@
private List<String> parseCommandsFromGerritHelpText(String helpText) {
List<String> commands = new ArrayList<>();
- String[] lines = helpText.split("\\n");
+ List<String> lines = Splitter.on("\n").splitToList(helpText);
// Skip all lines including the line starting with "Available commands"
int row = 0;
do {
row++;
- } while (row < lines.length && !lines[row - 1].startsWith("Available commands"));
+ } while (row < lines.size() && !lines.get(row - 1).startsWith("Available commands"));
// Skip all empty lines
- while (lines[row].trim().isEmpty()) {
+ while (lines.get(row).trim().isEmpty()) {
row++;
}
// Parse commands from all lines that are indented (start with a space)
- while (row < lines.length && lines[row].startsWith(" ")) {
- String line = lines[row].trim();
+ while (row < lines.size() && lines.get(row).startsWith(" ")) {
+ String line = lines.get(row).trim();
// Abort on empty line
if (line.isEmpty()) {
break;
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
index a003f9d..473b128 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
@@ -31,7 +31,7 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.truth.NullAwareCorrespondence;
import com.google.inject.Inject;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Optional;
import org.junit.Test;
@@ -325,9 +325,9 @@
GroupInfo group = gApi.groups().create(createArbitraryGroupInput()).detail();
AccountGroup.UUID groupUuid = AccountGroup.uuid(group.id);
- Timestamp createdOn = groupOperations.group(groupUuid).get().createdOn();
+ Instant createdOn = groupOperations.group(groupUuid).get().createdOn();
- assertThat(createdOn).isEqualTo(group.createdOn);
+ assertThat(createdOn).isEqualTo(group.createdOn.toInstant());
}
@Test
diff --git a/javatests/com/google/gerrit/common/BUILD b/javatests/com/google/gerrit/common/BUILD
index c7b21a3..492d007 100644
--- a/javatests/com/google/gerrit/common/BUILD
+++ b/javatests/com/google/gerrit/common/BUILD
@@ -11,6 +11,7 @@
"//lib:guava",
"//lib/auto:auto-value",
"//lib/auto:auto-value-annotations",
+ "//lib/log:log4j",
"//lib/truth",
],
)
diff --git a/javatests/com/google/gerrit/entities/LabelFunctionTest.java b/javatests/com/google/gerrit/entities/LabelFunctionTest.java
index 0132697..80d97db 100644
--- a/javatests/com/google/gerrit/entities/LabelFunctionTest.java
+++ b/javatests/com/google/gerrit/entities/LabelFunctionTest.java
@@ -19,7 +19,6 @@
import com.google.common.collect.ImmutableList;
import java.time.Instant;
import java.util.ArrayList;
-import java.util.Date;
import java.util.List;
import org.junit.Test;
@@ -94,7 +93,7 @@
return PatchSetApproval.builder()
.key(PatchSetApproval.key(PS_ID, Account.id(10000 + value), LABEL_ID))
.value(value)
- .granted(Date.from(Instant.now()))
+ .granted(Instant.now())
.build();
}
diff --git a/javatests/com/google/gerrit/entities/converter/ChangeMessageProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ChangeMessageProtoConverterTest.java
index ec6c372..22daf5b 100644
--- a/javatests/com/google/gerrit/entities/converter/ChangeMessageProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/ChangeMessageProtoConverterTest.java
@@ -27,7 +27,7 @@
import com.google.gerrit.proto.testing.SerializedClassSubject;
import com.google.gerrit.server.util.AccountTemplateUtil;
import java.lang.reflect.Type;
-import java.sql.Timestamp;
+import java.time.Instant;
import org.junit.Test;
public class ChangeMessageProtoConverterTest {
@@ -40,7 +40,7 @@
ChangeMessage.create(
ChangeMessage.key(Change.id(543), "change-message-21"),
Account.id(63),
- new Timestamp(9876543),
+ Instant.ofEpochMilli(9876543),
PatchSet.id(Change.id(34), 13),
"This is a change message.",
Account.id(10003),
@@ -73,7 +73,7 @@
ChangeMessage.create(
ChangeMessage.key(Change.id(543), "change-message-21"),
Account.id(63),
- new Timestamp(9876543),
+ Instant.ofEpochMilli(9876543),
PatchSet.id(Change.id(34), 13));
Entities.ChangeMessage proto = changeMessageProtoConverter.toProto(changeMessage);
@@ -140,7 +140,7 @@
ChangeMessage.create(
ChangeMessage.key(Change.id(543), "change-message-21"),
Account.id(63),
- new Timestamp(9876543),
+ Instant.ofEpochMilli(9876543),
PatchSet.id(Change.id(34), 13),
"This is a change message.",
Account.id(10003),
@@ -157,7 +157,7 @@
ChangeMessage.create(
ChangeMessage.key(Change.id(543), "change-message-21"),
Account.id(63),
- new Timestamp(9876543),
+ Instant.ofEpochMilli(9876543),
PatchSet.id(Change.id(34), 13),
String.format(
"This is a change message by %s and includes %s ",
@@ -178,7 +178,7 @@
ChangeMessage.create(
ChangeMessage.key(Change.id(543), "change-message-21"),
Account.id(63),
- new Timestamp(9876543),
+ Instant.ofEpochMilli(9876543),
PatchSet.id(Change.id(34), 13));
ChangeMessage convertedChangeMessage =
@@ -205,7 +205,7 @@
ImmutableMap.<String, Type>builder()
.put("key", ChangeMessage.Key.class)
.put("author", Account.Id.class)
- .put("writtenOn", Timestamp.class)
+ .put("writtenOn", Instant.class)
.put("message", String.class)
.put("patchset", PatchSet.Id.class)
.put("tag", String.class)
diff --git a/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java
index 8c5e449..bd4b2b1 100644
--- a/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java
@@ -27,7 +27,7 @@
import com.google.gerrit.proto.Entities;
import com.google.gerrit.proto.testing.SerializedClassSubject;
import java.lang.reflect.Type;
-import java.sql.Timestamp;
+import java.time.Instant;
import org.junit.Test;
public class ChangeProtoConverterTest {
@@ -41,8 +41,8 @@
Change.id(14),
Account.id(35),
BranchNameKey.create(Project.nameKey("project 67"), "branch 74"),
- new Timestamp(987654L));
- change.setLastUpdatedOn(new Timestamp(1234567L));
+ Instant.ofEpochMilli(987654L));
+ change.setLastUpdatedOn(Instant.ofEpochMilli(1234567L));
change.setStatus(Change.Status.MERGED);
change.setCurrentPatchSet(
PatchSet.id(Change.id(14), 23), "subject XYZ", "original subject ABC");
@@ -90,7 +90,7 @@
Change.id(14),
Account.id(35),
BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
- new Timestamp(987654L));
+ Instant.ofEpochMilli(987654L));
Entities.Change proto = changeProtoConverter.toProto(change);
@@ -125,7 +125,7 @@
Change.id(14),
Account.id(35),
BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
- new Timestamp(987654L));
+ Instant.ofEpochMilli(987654L));
// O as ID actually means that no current patch set is present.
change.setCurrentPatchSet(PatchSet.id(Change.id(14), 0), null, null);
@@ -162,7 +162,7 @@
Change.id(14),
Account.id(35),
BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
- new Timestamp(987654L));
+ Instant.ofEpochMilli(987654L));
change.setCurrentPatchSet(PatchSet.id(Change.id(14), 23), "subject ABC", null);
Entities.Change proto = changeProtoConverter.toProto(change);
@@ -198,8 +198,8 @@
Change.id(14),
Account.id(35),
BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
- new Timestamp(987654L));
- change.setLastUpdatedOn(new Timestamp(1234567L));
+ Instant.ofEpochMilli(987654L));
+ change.setLastUpdatedOn(Instant.ofEpochMilli(1234567L));
change.setStatus(Change.Status.MERGED);
change.setCurrentPatchSet(
PatchSet.id(Change.id(14), 23), "subject XYZ", "original subject ABC");
@@ -223,7 +223,7 @@
Change.id(14),
Account.id(35),
BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
- new Timestamp(987654L));
+ Instant.ofEpochMilli(987654L));
Change convertedChange = changeProtoConverter.fromProto(changeProtoConverter.toProto(change));
assertEqualChange(convertedChange, change);
@@ -242,8 +242,8 @@
assertThat(change.getKey()).isNull();
assertThat(change.getOwner()).isNull();
assertThat(change.getDest()).isNull();
- assertThat(change.getCreatedOn()).isEqualTo(new Timestamp(0));
- assertThat(change.getLastUpdatedOn()).isEqualTo(new Timestamp(0));
+ assertThat(change.getCreatedOn()).isEqualTo(Instant.EPOCH);
+ assertThat(change.getLastUpdatedOn()).isEqualTo(Instant.EPOCH);
assertThat(change.getSubject()).isNull();
assertThat(change.currentPatchSetId()).isNull();
// Default values for unset protobuf fields which can't be unset in the entity object.
@@ -268,7 +268,7 @@
.build();
Change change = changeProtoConverter.fromProto(proto);
- assertThat(change.getLastUpdatedOn()).isEqualTo(new Timestamp(987654L));
+ assertThat(change.getLastUpdatedOn()).isEqualTo(Instant.ofEpochMilli(987654L));
}
/** See {@link SerializedClassSubject} for background and what to do if this test fails. */
@@ -279,8 +279,8 @@
ImmutableMap.<String, Type>builder()
.put("changeId", Change.Id.class)
.put("changeKey", Change.Key.class)
- .put("createdOn", Timestamp.class)
- .put("lastUpdatedOn", Timestamp.class)
+ .put("createdOn", Instant.class)
+ .put("lastUpdatedOn", Instant.class)
.put("owner", Account.Id.class)
.put("dest", BranchNameKey.class)
.put("status", char.class)
diff --git a/javatests/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverterTest.java
index d332f8a..28f9cdb 100644
--- a/javatests/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverterTest.java
@@ -28,8 +28,7 @@
import com.google.gerrit.proto.testing.SerializedClassSubject;
import com.google.inject.TypeLiteral;
import java.lang.reflect.Type;
-import java.sql.Timestamp;
-import java.util.Date;
+import java.time.Instant;
import java.util.Optional;
import org.junit.Test;
@@ -44,8 +43,9 @@
.key(
PatchSetApproval.key(
PatchSet.id(Change.id(42), 14), Account.id(100013), LabelId.create("label-8")))
+ .uuid(Optional.of(PatchSetApproval.uuid("577fb248e474018276351785930358ec0450e9f7")))
.value(456)
- .granted(new Date(987654L))
+ .granted(Instant.ofEpochMilli(987654L))
.tag("tag-21")
.realAccountId(Account.id(612))
.postSubmit(true)
@@ -64,6 +64,7 @@
.setId(14))
.setAccountId(Entities.Account_Id.newBuilder().setId(100013))
.setLabelId(Entities.LabelId.newBuilder().setId("label-8")))
+ .setUuid("577fb248e474018276351785930358ec0450e9f7")
.setValue(456)
.setGranted(987654L)
.setTag("tag-21")
@@ -82,7 +83,7 @@
PatchSetApproval.key(
PatchSet.id(Change.id(42), 14), Account.id(100013), LabelId.create("label-8")))
.value(456)
- .granted(new Date(987654L))
+ .granted(Instant.ofEpochMilli(987654L))
.build();
Entities.PatchSetApproval proto = protoConverter.toProto(patchSetApproval);
@@ -113,8 +114,9 @@
.key(
PatchSetApproval.key(
PatchSet.id(Change.id(42), 14), Account.id(100013), LabelId.create("label-8")))
+ .uuid(Optional.of(PatchSetApproval.uuid("577fb248e474018276351785930358ec0450e9f7")))
.value(456)
- .granted(new Date(987654L))
+ .granted(Instant.ofEpochMilli(987654L))
.tag("tag-21")
.realAccountId(Account.id(612))
.postSubmit(true)
@@ -134,7 +136,7 @@
PatchSetApproval.key(
PatchSet.id(Change.id(42), 14), Account.id(100013), LabelId.create("label-8")))
.value(456)
- .granted(new Date(987654L))
+ .granted(Instant.ofEpochMilli(987654L))
.build();
PatchSetApproval convertedPatchSetApproval =
@@ -164,7 +166,7 @@
assertThat(patchSetApproval.labelId()).isEqualTo(LabelId.create("label-8"));
// Default values for unset protobuf fields which can't be unset in the entity object.
assertThat(patchSetApproval.value()).isEqualTo(0);
- assertThat(patchSetApproval.granted()).isEqualTo(new Timestamp(0));
+ assertThat(patchSetApproval.granted()).isEqualTo(Instant.EPOCH);
assertThat(patchSetApproval.postSubmit()).isEqualTo(false);
assertThat(patchSetApproval.copied()).isEqualTo(false);
}
@@ -176,8 +178,9 @@
.hasAutoValueMethods(
ImmutableMap.<String, Type>builder()
.put("key", PatchSetApproval.Key.class)
+ .put("uuid", new TypeLiteral<Optional<PatchSetApproval.UUID>>() {}.getType())
.put("value", short.class)
- .put("granted", Timestamp.class)
+ .put("granted", Instant.class)
.put("tag", new TypeLiteral<Optional<String>>() {}.getType())
.put("realAccountId", Account.Id.class)
.put("postSubmit", boolean.class)
diff --git a/javatests/com/google/gerrit/entities/converter/PatchSetProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/PatchSetProtoConverterTest.java
index efeb24f..3a534e9 100644
--- a/javatests/com/google/gerrit/entities/converter/PatchSetProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/PatchSetProtoConverterTest.java
@@ -27,7 +27,7 @@
import com.google.gerrit.proto.testing.SerializedClassSubject;
import com.google.inject.TypeLiteral;
import java.lang.reflect.Type;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Optional;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.Test;
@@ -42,7 +42,7 @@
.id(PatchSet.id(Change.id(103), 73))
.commitId(ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"))
.uploader(Account.id(452))
- .createdOn(new Timestamp(930349320L))
+ .createdOn(Instant.ofEpochMilli(930349320L))
.groups(ImmutableList.of("group1", " group2"))
.pushCertificate("my push certificate")
.description("This is a patch set description.")
@@ -74,7 +74,7 @@
.id(PatchSet.id(Change.id(103), 73))
.commitId(ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"))
.uploader(Account.id(452))
- .createdOn(new Timestamp(930349320L))
+ .createdOn(Instant.ofEpochMilli(930349320L))
.build();
Entities.PatchSet proto = patchSetProtoConverter.toProto(patchSet);
@@ -100,7 +100,7 @@
.id(PatchSet.id(Change.id(103), 73))
.commitId(ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"))
.uploader(Account.id(452))
- .createdOn(new Timestamp(930349320L))
+ .createdOn(Instant.ofEpochMilli(930349320L))
.groups(ImmutableList.of("group1", " group2"))
.pushCertificate("my push certificate")
.description("This is a patch set description.")
@@ -118,7 +118,7 @@
.id(PatchSet.id(Change.id(103), 73))
.commitId(ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"))
.uploader(Account.id(452))
- .createdOn(new Timestamp(930349320L))
+ .createdOn(Instant.ofEpochMilli(930349320L))
.build();
PatchSet convertedPatchSet =
@@ -143,7 +143,7 @@
.id(PatchSet.id(Change.id(103), 73))
.commitId(ObjectId.fromString("0000000000000000000000000000000000000000"))
.uploader(Account.id(0))
- .createdOn(new Timestamp(0))
+ .createdOn(Instant.EPOCH)
.build());
}
@@ -156,7 +156,7 @@
.put("id", PatchSet.Id.class)
.put("commitId", ObjectId.class)
.put("uploader", Account.Id.class)
- .put("createdOn", Timestamp.class)
+ .put("createdOn", Instant.class)
.put("groups", new TypeLiteral<ImmutableList<String>>() {}.getType())
.put("pushCertificate", new TypeLiteral<Optional<String>>() {}.getType())
.put("description", new TypeLiteral<Optional<String>>() {}.getType())
diff --git a/javatests/com/google/gerrit/git/RefUpdateUtilTest.java b/javatests/com/google/gerrit/git/RefUpdateUtilTest.java
index 1d021f7..a1f6cef 100644
--- a/javatests/com/google/gerrit/git/RefUpdateUtilTest.java
+++ b/javatests/com/google/gerrit/git/RefUpdateUtilTest.java
@@ -33,44 +33,38 @@
@RunWith(JUnit4.class)
public class RefUpdateUtilTest {
- private static final Consumer<ReceiveCommand> OK = c -> c.setResult(ReceiveCommand.Result.OK);
- private static final Consumer<ReceiveCommand> LOCK_FAILURE =
- c -> c.setResult(ReceiveCommand.Result.LOCK_FAILURE);
- private static final Consumer<ReceiveCommand> REJECTED =
- c -> c.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON);
- private static final Consumer<ReceiveCommand> ABORTED =
- c -> {
- c.setResult(ReceiveCommand.Result.NOT_ATTEMPTED);
- ReceiveCommand.abort(ImmutableList.of(c));
- checkState(
- c.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED
- && c.getResult() != ReceiveCommand.Result.LOCK_FAILURE
- && c.getResult() != ReceiveCommand.Result.OK,
- "unexpected state after abort: %s",
- c);
- };
-
@Test
public void checkBatchRefUpdateResults() throws Exception {
checkResults();
- checkResults(OK);
- checkResults(OK, OK);
+ checkResults(RefUpdateUtilTest::ok);
+ checkResults(RefUpdateUtilTest::ok, RefUpdateUtilTest::ok);
- assertIoException(REJECTED);
- assertIoException(OK, REJECTED);
- assertIoException(LOCK_FAILURE, REJECTED);
- assertIoException(LOCK_FAILURE, OK);
- assertIoException(LOCK_FAILURE, REJECTED, OK);
- assertIoException(LOCK_FAILURE, LOCK_FAILURE, REJECTED);
- assertIoException(LOCK_FAILURE, ABORTED, REJECTED);
- assertIoException(LOCK_FAILURE, ABORTED, OK);
+ assertIoException(RefUpdateUtilTest::rejected);
+ assertIoException(RefUpdateUtilTest::ok, RefUpdateUtilTest::rejected);
+ assertIoException(RefUpdateUtilTest::lockFailure, RefUpdateUtilTest::rejected);
+ assertIoException(RefUpdateUtilTest::lockFailure, RefUpdateUtilTest::ok);
+ assertIoException(
+ RefUpdateUtilTest::lockFailure, RefUpdateUtilTest::rejected, RefUpdateUtilTest::ok);
+ assertIoException(
+ RefUpdateUtilTest::lockFailure,
+ RefUpdateUtilTest::lockFailure,
+ RefUpdateUtilTest::rejected);
+ assertIoException(
+ RefUpdateUtilTest::lockFailure, RefUpdateUtilTest::aborted, RefUpdateUtilTest::rejected);
+ assertIoException(
+ RefUpdateUtilTest::lockFailure, RefUpdateUtilTest::aborted, RefUpdateUtilTest::ok);
- assertLockFailureException(LOCK_FAILURE);
- assertLockFailureException(LOCK_FAILURE, LOCK_FAILURE);
- assertLockFailureException(LOCK_FAILURE, LOCK_FAILURE, ABORTED);
- assertLockFailureException(LOCK_FAILURE, LOCK_FAILURE, ABORTED, ABORTED);
- assertLockFailureException(ABORTED);
- assertLockFailureException(ABORTED, ABORTED);
+ assertLockFailureException(RefUpdateUtilTest::lockFailure);
+ assertLockFailureException(RefUpdateUtilTest::lockFailure, RefUpdateUtilTest::lockFailure);
+ assertLockFailureException(
+ RefUpdateUtilTest::lockFailure, RefUpdateUtilTest::lockFailure, RefUpdateUtilTest::aborted);
+ assertLockFailureException(
+ RefUpdateUtilTest::lockFailure,
+ RefUpdateUtilTest::lockFailure,
+ RefUpdateUtilTest::aborted,
+ RefUpdateUtilTest::aborted);
+ assertLockFailureException(RefUpdateUtilTest::aborted);
+ assertLockFailureException(RefUpdateUtilTest::aborted, RefUpdateUtilTest::aborted);
}
@SafeVarargs
@@ -110,4 +104,27 @@
return bru;
}
}
+
+ private static void ok(ReceiveCommand c) {
+ c.setResult(ReceiveCommand.Result.OK);
+ }
+
+ private static void lockFailure(ReceiveCommand c) {
+ c.setResult(ReceiveCommand.Result.LOCK_FAILURE);
+ }
+
+ private static void rejected(ReceiveCommand c) {
+ c.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON);
+ }
+
+ private static void aborted(ReceiveCommand c) {
+ c.setResult(ReceiveCommand.Result.NOT_ATTEMPTED);
+ ReceiveCommand.abort(ImmutableList.of(c));
+ checkState(
+ c.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED
+ && c.getResult() != ReceiveCommand.Result.LOCK_FAILURE
+ && c.getResult() != ReceiveCommand.Result.OK,
+ "unexpected state after abort: %s",
+ c);
+ }
}
diff --git a/javatests/com/google/gerrit/gpg/PublicKeyCheckerTest.java b/javatests/com/google/gerrit/gpg/PublicKeyCheckerTest.java
index 7703fb0..8bafafe 100644
--- a/javatests/com/google/gerrit/gpg/PublicKeyCheckerTest.java
+++ b/javatests/com/google/gerrit/gpg/PublicKeyCheckerTest.java
@@ -38,11 +38,12 @@
import static org.junit.Assert.assertEquals;
import com.google.gerrit.gpg.testing.TestKey;
-import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -212,9 +213,11 @@
String problem = "Key is revoked (key material has been compromised): test6 compromised";
assertProblems(k, problem);
- SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- PublicKeyChecker checker =
- new PublicKeyChecker().setStore(store).setEffectiveTime(df.parse("2010-01-01 12:00:00"));
+ Instant instant =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
+ .withZone(ZoneId.systemDefault())
+ .parse("2010-01-01 12:00:00", Instant::from);
+ PublicKeyChecker checker = new PublicKeyChecker().setStore(store).setEffectiveTime(instant);
assertProblems(checker, k, problem);
}
@@ -360,8 +363,8 @@
+ " is valid, but key is not trusted";
}
- private static Date parseDate(String str) throws Exception {
- return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").parse(str);
+ private static Instant parseDate(String str) throws Exception {
+ return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z").parse(str, Instant::from);
}
private static List<String> list(String first, String[] rest) {
diff --git a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
index 6691587..cc1ee00 100644
--- a/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/IndexHtmlUtilTest.java
@@ -116,7 +116,7 @@
assertThat(dynamicTemplateData(gerritApi, "/c/project/+/123"))
.containsAtLeast(
- "defaultChangeDetailHex", "916314",
+ "defaultChangeDetailHex", "1916314",
"changeRequestsPath", "changes/project~123");
}
diff --git a/javatests/com/google/gerrit/index/query/PredicateTest.java b/javatests/com/google/gerrit/index/query/PredicateTest.java
index 3ec7f13..171ca27 100644
--- a/javatests/com/google/gerrit/index/query/PredicateTest.java
+++ b/javatests/com/google/gerrit/index/query/PredicateTest.java
@@ -19,7 +19,7 @@
@Ignore
public abstract class PredicateTest {
protected static final class TestPredicate extends OperatorPredicate<String> {
- protected TestPredicate(String name, String value) {
+ private TestPredicate(String name, String value) {
super(name, value);
}
}
diff --git a/javatests/com/google/gerrit/integration/ssh/PeerKeysAuthIT.java b/javatests/com/google/gerrit/integration/ssh/PeerKeysAuthIT.java
index a219cc2..039bc8e 100644
--- a/javatests/com/google/gerrit/integration/ssh/PeerKeysAuthIT.java
+++ b/javatests/com/google/gerrit/integration/ssh/PeerKeysAuthIT.java
@@ -19,6 +19,7 @@
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.acceptance.GerritServer.TestSshServerAddress;
@@ -31,6 +32,7 @@
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.file.Files;
+import java.util.List;
import org.junit.Test;
@NoHttpd
@@ -59,17 +61,19 @@
// Generate private/public key for user
execute(ImmutableList.<String>builder().add(SSH_KEYGEN_CMD).build());
- String[] parts =
- new String(Files.readAllBytes(sitePaths.data_dir.resolve("id_rsa.pub")), UTF_8)
- .split(" ");
+ List<String> parts =
+ Splitter.on(" ")
+ .splitToList(
+ new String(Files.readAllBytes(sitePaths.data_dir.resolve("id_rsa.pub")), UTF_8));
// Loose algorithm at index 0, verify the format: "key comment"
Files.write(
- sitePaths.peer_keys, String.format("%s %s", parts[1], parts[2]).getBytes(ISO_8859_1));
+ sitePaths.peer_keys,
+ String.format("%s %s", parts.get(1), parts.get(2)).getBytes(ISO_8859_1));
assertContent(execGerritVersionCommand());
// Only preserve the key material: no algorithm and no comment
- Files.write(sitePaths.peer_keys, parts[1].getBytes(ISO_8859_1));
+ Files.write(sitePaths.peer_keys, parts.get(1).getBytes(ISO_8859_1));
assertContent(execGerritVersionCommand());
// Wipe out the content of the peer keys file
diff --git a/javatests/com/google/gerrit/json/SqlTimestampDeserializerTest.java b/javatests/com/google/gerrit/json/SqlTimestampDeserializerTest.java
index 2699c3b..44ef822 100644
--- a/javatests/com/google/gerrit/json/SqlTimestampDeserializerTest.java
+++ b/javatests/com/google/gerrit/json/SqlTimestampDeserializerTest.java
@@ -16,9 +16,9 @@
import static com.google.common.truth.Truth.assertThat;
-import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gson.JsonPrimitive;
import java.sql.Timestamp;
+import java.time.Instant;
import org.junit.Test;
public class SqlTimestampDeserializerTest {
@@ -28,6 +28,6 @@
@Test
public void emptyStringIsDeserializedToMagicTimestamp() {
Timestamp timestamp = deserializer.deserialize(new JsonPrimitive(""), Timestamp.class, null);
- assertThat(timestamp).isEqualTo(TimeUtil.never());
+ assertThat(timestamp).isEqualTo(Timestamp.from(Instant.EPOCH));
}
}
diff --git a/javatests/com/google/gerrit/mail/AbstractParserTest.java b/javatests/com/google/gerrit/mail/AbstractParserTest.java
index a2432a2..f99a2af 100644
--- a/javatests/com/google/gerrit/mail/AbstractParserTest.java
+++ b/javatests/com/google/gerrit/mail/AbstractParserTest.java
@@ -20,7 +20,6 @@
import com.google.gerrit.entities.Address;
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.HumanComment;
-import java.sql.Timestamp;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@@ -58,7 +57,7 @@
new HumanComment(
new Comment.Key(uuid, file, 1),
Account.id(0),
- new Timestamp(0L),
+ Instant.EPOCH,
(short) 0,
message,
"",
@@ -73,7 +72,7 @@
new HumanComment(
new Comment.Key(uuid, file, 1),
Account.id(0),
- new Timestamp(0L),
+ Instant.EPOCH,
(short) 0,
message,
"",
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index 897691b..87d1729 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -58,6 +58,7 @@
"//java/com/google/gerrit/proto/testing",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server/account/externalids/testing",
+ "//java/com/google/gerrit/server/cache/mem",
"//java/com/google/gerrit/server/cache/serialize",
"//java/com/google/gerrit/server/cache/testing",
"//java/com/google/gerrit/server/cancellation",
diff --git a/javatests/com/google/gerrit/server/IdentifiedUserTest.java b/javatests/com/google/gerrit/server/IdentifiedUserTest.java
index 463af35..855a0bc 100644
--- a/javatests/com/google/gerrit/server/IdentifiedUserTest.java
+++ b/javatests/com/google/gerrit/server/IdentifiedUserTest.java
@@ -99,7 +99,7 @@
injector.injectMembers(this);
Account account =
- Account.builder(Account.id(1), TimeUtil.nowTs())
+ Account.builder(Account.id(1), TimeUtil.now())
.setMetaId("1234567812345678123456781234567812345678")
.build();
Account.Id ownerId = account.id();
diff --git a/javatests/com/google/gerrit/server/account/AccountCacheTest.java b/javatests/com/google/gerrit/server/account/AccountCacheTest.java
index a2aa40b..c1eff15 100644
--- a/javatests/com/google/gerrit/server/account/AccountCacheTest.java
+++ b/javatests/com/google/gerrit/server/account/AccountCacheTest.java
@@ -23,7 +23,6 @@
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.cache.proto.Cache;
import com.google.gerrit.server.config.CachedPreferences;
-import java.sql.Timestamp;
import java.time.Instant;
import org.junit.Test;
@@ -32,8 +31,7 @@
* of the {@code AccountCache}.
*/
public class AccountCacheTest {
- private static final Account ACCOUNT =
- Account.builder(Account.id(1), Timestamp.from(Instant.EPOCH)).build();
+ private static final Account ACCOUNT = Account.builder(Account.id(1), Instant.EPOCH).build();
private static final Cache.AccountProto ACCOUNT_PROTO =
Cache.AccountProto.newBuilder().setId(1).setRegisteredOn(0).build();
private static final CachedAccountDetails.Serializer SERIALIZER =
@@ -42,7 +40,7 @@
@Test
public void account_roundTrip() throws Exception {
Account account =
- Account.builder(Account.id(1), Timestamp.from(Instant.EPOCH))
+ Account.builder(Account.id(1), Instant.EPOCH)
.setFullName("foo bar")
.setDisplayName("foo")
.setActive(false)
diff --git a/javatests/com/google/gerrit/server/account/AccountResolverTest.java b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
index 5e49aaf..3658834 100644
--- a/javatests/com/google/gerrit/server/account/AccountResolverTest.java
+++ b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
@@ -96,15 +96,15 @@
new TestSearcher("foo", false, newAccount(1)),
new TestSearcher("bar", false, newAccount(2), newAccount(3)));
- Result result = search("foo", searchers, allVisible());
+ Result result = search("foo", searchers, AccountResolverTest::allVisiblePredicate);
assertThat(result.input()).isEqualTo("foo");
assertThat(result.asIdSet()).containsExactlyElementsIn(ids(1));
- result = search("bar", searchers, allVisible());
+ result = search("bar", searchers, AccountResolverTest::allVisiblePredicate);
assertThat(result.input()).isEqualTo("bar");
assertThat(result.asIdSet()).containsExactlyElementsIn(ids(2, 3));
- result = search("baz", searchers, allVisible());
+ result = search("baz", searchers, AccountResolverTest::allVisiblePredicate);
assertThat(result.input()).isEqualTo("baz");
assertThat(result.asIdSet()).isEmpty();
}
@@ -115,11 +115,11 @@
ImmutableList.of(
new TestSearcher("f.*", true), new TestSearcher("foo|bar", false, newAccount(1)));
- Result result = search("foo", searchers, allVisible());
+ Result result = search("foo", searchers, AccountResolverTest::allVisiblePredicate);
assertThat(result.input()).isEqualTo("foo");
assertThat(result.asIdSet()).isEmpty();
- result = search("bar", searchers, allVisible());
+ result = search("bar", searchers, AccountResolverTest::allVisiblePredicate);
assertThat(result.input()).isEqualTo("bar");
assertThat(result.asIdSet()).containsExactlyElementsIn(ids(1));
}
@@ -129,7 +129,7 @@
ImmutableList<Searcher<?>> searchers =
ImmutableList.of(new TestSearcher("foo", false, newAccount(1), newAccount(2)));
- assertThat(search("foo", searchers, allVisible()).asIdSet())
+ assertThat(search("foo", searchers, AccountResolverTest::allVisiblePredicate).asIdSet())
.containsExactlyElementsIn(ids(1, 2));
assertThat(search("foo", searchers, only(2)).asIdSet()).containsExactlyElementsIn(ids(2));
}
@@ -152,7 +152,7 @@
new TestSearcher("foo", false, newInactiveAccount(1)),
new TestSearcher("f.*", false, newInactiveAccount(2)));
- Result result = search("foo", searchers, allVisible());
+ Result result = search("foo", searchers, AccountResolverTest::allVisiblePredicate);
// Searchers always short-circuit when finding a non-empty result list, and this one didn't
// filter out inactive results, so the second searcher never ran.
assertThat(result.asIdSet()).containsExactlyElementsIn(ids(1));
@@ -168,7 +168,7 @@
searcher2.setCallerShouldFilterOutInactiveCandidates();
ImmutableList<Searcher<?>> searchers = ImmutableList.of(searcher1, searcher2);
- Result result = search("foo", searchers, allVisible(), (a) -> true);
+ Result result = search("foo", searchers, AccountResolverTest::allVisiblePredicate, (a) -> true);
// Searchers always short-circuit when finding a non-empty result list,
// and this one didn't filter out inactive results,
// so the second searcher never ran.
@@ -185,8 +185,9 @@
searcher2.setCallerShouldFilterOutInactiveCandidates();
ImmutableList<Searcher<?>> searchers = ImmutableList.of(searcher1, searcher2);
- Result result = search("foo", searchers, allVisible());
- assertThat(search("foo", searchers, allVisible()).asIdSet()).containsExactlyElementsIn(ids(2));
+ Result result = search("foo", searchers, AccountResolverTest::allVisiblePredicate);
+ assertThat(search("foo", searchers, AccountResolverTest::allVisiblePredicate).asIdSet())
+ .containsExactlyElementsIn(ids(2));
// No info about inactive results exposed if there was at least one active result.
assertThat(filteredInactiveIds(result)).isEmpty();
}
@@ -199,7 +200,7 @@
searcher2.setCallerShouldFilterOutInactiveCandidates();
ImmutableList<Searcher<?>> searchers = ImmutableList.of(searcher1, searcher2);
- Result result = search("foo", searchers, allVisible());
+ Result result = search("foo", searchers, AccountResolverTest::allVisiblePredicate);
assertThat(result.asIdSet()).isEmpty();
assertThat(filteredInactiveIds(result)).containsExactlyElementsIn(ids(1, 2));
}
@@ -217,7 +218,7 @@
// searcher1 matched, but filtered out all candidates because account2 is inactive. Actual
// result came from searcher2 instead.
- Result result = search("foo", searchers, allVisible());
+ Result result = search("foo", searchers, AccountResolverTest::allVisiblePredicate);
assertThat(result.asIdSet()).containsExactlyElementsIn(ids(1, 2));
}
@@ -233,7 +234,7 @@
// searcher1 matched and then filtered out all candidates because account2 is inactive, but
// still short-circuited.
- Result result = search("foo", searchers, allVisible());
+ Result result = search("foo", searchers, AccountResolverTest::allVisiblePredicate);
assertThat(result.asIdSet()).isEmpty();
assertThat(filteredInactiveIds(result)).containsExactlyElementsIn(ids(2));
}
@@ -242,11 +243,10 @@
public void asUniqueWithNoResults() throws Exception {
String input = "foo";
ImmutableList<Searcher<?>> searchers = ImmutableList.of();
- Supplier<Predicate<AccountState>> visibilitySupplier = allVisible();
UnresolvableAccountException thrown =
assertThrows(
UnresolvableAccountException.class,
- () -> search(input, searchers, visibilitySupplier).asUnique());
+ () -> search(input, searchers, AccountResolverTest::allVisiblePredicate).asUnique());
assertThat(thrown).hasMessageThat().isEqualTo("Account 'foo' not found");
}
@@ -255,7 +255,11 @@
AccountState account = newAccount(1);
ImmutableList<Searcher<?>> searchers =
ImmutableList.of(new TestSearcher("foo", false, account));
- assertThat(search("foo", searchers, allVisible()).asUnique().account().id())
+ assertThat(
+ search("foo", searchers, AccountResolverTest::allVisiblePredicate)
+ .asUnique()
+ .account()
+ .id())
.isEqualTo(account.account().id());
}
@@ -266,7 +270,7 @@
UnresolvableAccountException thrown =
assertThrows(
UnresolvableAccountException.class,
- () -> search("foo", searchers, allVisible()).asUnique());
+ () -> search("foo", searchers, AccountResolverTest::allVisiblePredicate).asUnique());
assertThat(thrown)
.hasMessageThat()
.isEqualTo(
@@ -339,7 +343,7 @@
ImmutableList<Searcher<?>> searchers,
Supplier<Predicate<AccountState>> visibilitySupplier)
throws Exception {
- return search(input, searchers, visibilitySupplier, activityPrediate());
+ return search(input, searchers, visibilitySupplier, AccountResolverTest::isActive);
}
private Result search(
@@ -357,13 +361,13 @@
private AccountState newAccount(int id) {
return AccountState.forAccount(
- Account.builder(Account.id(id), TimeUtil.nowTs())
+ Account.builder(Account.id(id), TimeUtil.now())
.setMetaId("1234567812345678123456781234567812345678")
.build());
}
private AccountState newInactiveAccount(int id) {
- Account.Builder a = Account.builder(Account.id(id), TimeUtil.nowTs());
+ Account.Builder a = Account.builder(Account.id(id), TimeUtil.now());
a.setActive(false);
return AccountState.forAccount(a.build());
}
@@ -372,12 +376,17 @@
return Arrays.stream(ids).mapToObj(Account::id).collect(toImmutableSet());
}
- private static Supplier<Predicate<AccountState>> allVisible() {
- return () -> a -> true;
+ private static Predicate<AccountState> allVisiblePredicate() {
+ return AccountResolverTest::allVisible;
}
- private Predicate<AccountState> activityPrediate() {
- return (AccountState accountState) -> accountState.account().isActive();
+ /** @param accountState account state for which the visibility should be checked */
+ private static boolean allVisible(AccountState accountState) {
+ return true;
+ }
+
+ private static boolean isActive(AccountState accountState) {
+ return accountState.account().isActive();
}
private static Supplier<Predicate<AccountState>> only(int... ids) {
diff --git a/javatests/com/google/gerrit/server/account/AuthorizedKeysTest.java b/javatests/com/google/gerrit/server/account/AuthorizedKeysTest.java
index 1381c75..1a7d1fb 100644
--- a/javatests/com/google/gerrit/server/account/AuthorizedKeysTest.java
+++ b/javatests/com/google/gerrit/server/account/AuthorizedKeysTest.java
@@ -16,6 +16,7 @@
import static com.google.common.truth.Truth.assertThat;
+import com.google.common.base.Splitter;
import com.google.gerrit.entities.Account;
import java.util.ArrayList;
import java.util.List;
@@ -125,18 +126,22 @@
public void getters() throws Exception {
AccountSshKey key = AccountSshKey.create(accountId, 1, KEY1);
assertThat(key.sshPublicKey()).isEqualTo(KEY1);
- assertThat(key.algorithm()).isEqualTo(KEY1.split(" ")[0]);
- assertThat(key.encodedKey()).isEqualTo(KEY1.split(" ")[1]);
- assertThat(key.comment()).isEqualTo(KEY1.split(" ")[2]);
+
+ List<String> keyParts = Splitter.on(" ").splitToList(KEY1);
+ assertThat(key.algorithm()).isEqualTo(keyParts.get(0));
+ assertThat(key.encodedKey()).isEqualTo(keyParts.get(1));
+ assertThat(key.comment()).isEqualTo(keyParts.get(2));
}
@Test
public void keyWithNewLines() throws Exception {
AccountSshKey key = AccountSshKey.create(accountId, 1, KEY1_WITH_NEWLINES);
assertThat(key.sshPublicKey()).isEqualTo(KEY1);
- assertThat(key.algorithm()).isEqualTo(KEY1.split(" ")[0]);
- assertThat(key.encodedKey()).isEqualTo(KEY1.split(" ")[1]);
- assertThat(key.comment()).isEqualTo(KEY1.split(" ")[2]);
+
+ List<String> keyParts = Splitter.on(" ").splitToList(KEY1);
+ assertThat(key.algorithm()).isEqualTo(keyParts.get(0));
+ assertThat(key.encodedKey()).isEqualTo(keyParts.get(1));
+ assertThat(key.comment()).isEqualTo(keyParts.get(2));
}
private static String toWindowsLineEndings(String s) {
diff --git a/javatests/com/google/gerrit/server/cache/PersistentCacheFactoryIT.java b/javatests/com/google/gerrit/server/cache/PersistentCacheFactoryIT.java
index 2264612..fb9a375 100644
--- a/javatests/com/google/gerrit/server/cache/PersistentCacheFactoryIT.java
+++ b/javatests/com/google/gerrit/server/cache/PersistentCacheFactoryIT.java
@@ -57,15 +57,13 @@
}
@Override
- public <K, V> com.google.common.cache.Cache<K, V> build(
- PersistentCacheDef<K, V> def, CacheBackend backend) {
- return memoryCacheFactory.build(def, backend);
+ public <K, V> com.google.common.cache.Cache<K, V> build(PersistentCacheDef<K, V> def) {
+ return memoryCacheFactory.build(def);
}
@Override
- public <K, V> LoadingCache<K, V> build(
- PersistentCacheDef<K, V> def, CacheLoader<K, V> loader, CacheBackend backend) {
- return memoryCacheFactory.build(def, loader, backend);
+ public <K, V> LoadingCache<K, V> build(PersistentCacheDef<K, V> def, CacheLoader<K, V> loader) {
+ return memoryCacheFactory.build(def, loader);
}
@Override
diff --git a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
index 14af43b..16fd4ca 100644
--- a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
+++ b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
@@ -45,7 +45,7 @@
import org.junit.Test;
public class H2CacheTest {
- private static final TypeLiteral<String> KEY_TYPE = new TypeLiteral<String>() {};
+ private static final TypeLiteral<String> KEY_TYPE = new TypeLiteral<>() {};
private static final int DEFAULT_VERSION = 1234;
private static int dbCnt;
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/BUILD b/javatests/com/google/gerrit/server/cache/serialize/entities/BUILD
index 8df9292..d68a5c1 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/entities/BUILD
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/BUILD
@@ -16,5 +16,6 @@
"//lib/truth:truth-proto-extension",
"//proto:cache_java_proto",
"//proto/testing:test_java_proto",
+ "@gson//jar",
],
)
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/InternalGroupSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/InternalGroupSerializerTest.java
index 8d301e4..78947a2 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/entities/InternalGroupSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/InternalGroupSerializerTest.java
@@ -34,7 +34,7 @@
.setOwnerGroupUUID(AccountGroup.uuid("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"))
.setVisibleToAll(false)
.setGroupUUID(AccountGroup.uuid("deadbeefdeadbeefdeadbeefdeadbeef12345678"))
- .setCreatedOn(TimeUtil.nowTs())
+ .setCreatedOn(TimeUtil.now())
.setMembers(ImmutableSet.of(Account.id(123), Account.id(321)))
.setSubgroups(
ImmutableSet.of(
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializerTest.java
index 614dcf0..b759fec 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/LabelTypeSerializerTest.java
@@ -21,6 +21,7 @@
import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelValue;
+import java.util.Optional;
import org.junit.Test;
public class LabelTypeSerializerTest {
@@ -30,6 +31,7 @@
ImmutableList.of(
LabelValue.create((short) 0, "no vote"),
LabelValue.create((short) 1, "approved")))
+ .setDescription(Optional.of("description"))
.setCanOverride(!LabelType.DEF_CAN_OVERRIDE)
.setAllowPostSubmit(!LabelType.DEF_ALLOW_POST_SUBMIT)
.setIgnoreSelfApproval(!LabelType.DEF_IGNORE_SELF_APPROVAL)
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementExpressionResultSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementExpressionResultSerializerTest.java
new file mode 100644
index 0000000..4705c55
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementExpressionResultSerializerTest.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.cache.serialize.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.cache.serialize.entities.SubmitRequirementExpressionResultSerializer.deserialize;
+import static com.google.gerrit.server.cache.serialize.entities.SubmitRequirementExpressionResultSerializer.serialize;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.SubmitRequirementExpression;
+import com.google.gerrit.entities.SubmitRequirementExpressionResult;
+import com.google.gerrit.entities.SubmitRequirementExpressionResult.Status;
+import java.util.Optional;
+import org.junit.Test;
+
+public class SubmitRequirementExpressionResultSerializerTest {
+ private static final SubmitRequirementExpressionResult r1 =
+ SubmitRequirementExpressionResult.create(
+ SubmitRequirementExpression.create("label:Code-Review=+2"),
+ Status.PASS,
+ ImmutableList.of("Label:Code-Review=+2"),
+ ImmutableList.of());
+
+ private static final SubmitRequirementExpressionResult r2 =
+ SubmitRequirementExpressionResult.create(
+ SubmitRequirementExpression.create("label:Code-Review=+2"),
+ Status.ERROR,
+ ImmutableList.of(),
+ ImmutableList.of(),
+ Optional.of("Failed to parse the code review label"));
+
+ @Test
+ public void roundTrip_withoutError() throws Exception {
+ assertThat(deserialize(serialize(r1))).isEqualTo(r1);
+ }
+
+ @Test
+ public void roundTrip_withErrorMessage() throws Exception {
+ assertThat(deserialize(serialize(r2))).isEqualTo(r2);
+ }
+}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementJsonSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementJsonSerializerTest.java
new file mode 100644
index 0000000..5cd43af
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementJsonSerializerTest.java
@@ -0,0 +1,237 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.cache.serialize.entities;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementExpression;
+import com.google.gerrit.entities.SubmitRequirementExpressionResult;
+import com.google.gerrit.entities.SubmitRequirementExpressionResult.Status;
+import com.google.gerrit.entities.SubmitRequirementResult;
+import com.google.gerrit.server.notedb.ChangeNoteJson;
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+public class SubmitRequirementJsonSerializerTest {
+ private static final SubmitRequirementExpression srReqExp =
+ SubmitRequirementExpression.create("label:Code-Review=+2");
+
+ private static final String srReqExpSerial = "{\"expressionString\":\"label:Code-Review=+2\"}";
+
+ private static final SubmitRequirement sr =
+ SubmitRequirement.builder()
+ .setName("CR")
+ .setDescription(Optional.of("CR description"))
+ .setApplicabilityExpression(SubmitRequirementExpression.of("branch:refs/heads/master"))
+ .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+2"))
+ .setAllowOverrideInChildProjects(true)
+ .build();
+
+ private static final String srSerial =
+ "{\"name\":\"CR\","
+ + "\"description\":{\"value\":\"CR description\"},"
+ + "\"applicabilityExpression\":{\"value\":"
+ + "{\"expressionString\":\"branch:refs/heads/master\"}},"
+ + "\"submittabilityExpression\":{"
+ + "\"expressionString\":\"label:Code-Review=+2\"},"
+ + "\"overrideExpression\":{\"value\":null},"
+ + "\"allowOverrideInChildProjects\":true}";
+
+ private static final SubmitRequirementExpressionResult srExpResult =
+ SubmitRequirementExpressionResult.create(
+ SubmitRequirementExpression.create("label:Code-Review=MAX AND -label:Code-Review=MIN"),
+ Status.FAIL,
+ /* passingAtoms= */ ImmutableList.of("label:Code-Review=MAX"),
+ /* failingAtoms= */ ImmutableList.of("label:Code-Review=MIN"));
+
+ private static final String srExpResultSerial =
+ "{\"expression\":{\"expressionString\":"
+ + "\"label:Code-Review=MAX AND -label:Code-Review=MIN\"},"
+ + "\"status\":\"FAIL\","
+ + "\"errorMessage\":{\"value\":null},"
+ + "\"passingAtoms\":[\"label:Code-Review=MAX\"],"
+ + "\"failingAtoms\":[\"label:Code-Review=MIN\"]}";
+
+ private static final SubmitRequirementResult srReqResult =
+ SubmitRequirementResult.builder()
+ .submitRequirement(
+ SubmitRequirement.builder()
+ .setName("CR")
+ .setDescription(Optional.of("CR Description"))
+ .setApplicabilityExpression(
+ SubmitRequirementExpression.of("branch:refs/heads/master"))
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create("label:\"Code-Review=+2\""))
+ .setOverrideExpression(SubmitRequirementExpression.of("label:Override=+1"))
+ .setAllowOverrideInChildProjects(false)
+ .build())
+ .patchSetCommitId(ObjectId.fromString("4663ab9e9eb49a214e68e60f0fe5d0b6f44f763e"))
+ .applicabilityExpressionResult(
+ Optional.of(
+ SubmitRequirementExpressionResult.create(
+ SubmitRequirementExpression.create("branch:refs/heads/master"),
+ Status.PASS,
+ ImmutableList.of("refs/heads/master"),
+ ImmutableList.of())))
+ .submittabilityExpressionResult(
+ SubmitRequirementExpressionResult.create(
+ SubmitRequirementExpression.create("label:\"Code-Review=+2\""),
+ Status.PASS,
+ /* passingAtoms= */ ImmutableList.of("label:\"Code-Review=+2\""),
+ /* failingAtoms= */ ImmutableList.of()))
+ .overrideExpressionResult(
+ Optional.of(
+ SubmitRequirementExpressionResult.create(
+ SubmitRequirementExpression.create("label:Override=+1"),
+ Status.PASS,
+ /* passingAtoms= */ ImmutableList.of(),
+ /* failingAtoms= */ ImmutableList.of("label:Override=+1"))))
+ .legacy(Optional.of(true))
+ .build();
+
+ private static final String srReqResultSerial =
+ "{\"submitRequirement\":{\"name\":\"CR\",\"description\":{\"value\":\"CR Description\"},"
+ + "\"applicabilityExpression\":{\"value\":{"
+ + "\"expressionString\":\"branch:refs/heads/master\"}},"
+ + "\"submittabilityExpression\":{\"expressionString\":\"label:\\\"Code-Review=+2\\\"\"},"
+ + "\"overrideExpression\":{\"value\":{\"expressionString\":\"label:Override=+1\"}},"
+ + "\"allowOverrideInChildProjects\":false},"
+ + "\"applicabilityExpressionResult\":{\"value\":{"
+ + "\"expression\":{\"expressionString\":\"branch:refs/heads/master\"},"
+ + "\"status\":\"PASS\",\"errorMessage\":{\"value\":null},"
+ + "\"passingAtoms\":[\"refs/heads/master\"],"
+ + "\"failingAtoms\":[]}},"
+ + "\"submittabilityExpressionResult\":{"
+ + "\"expression\":{\"expressionString\":\"label:\\\"Code-Review=+2\\\"\"},"
+ + "\"status\":\"PASS\",\"errorMessage\":{\"value\":null},"
+ + "\"passingAtoms\":[\"label:\\\"Code-Review=+2\\\"\"],"
+ + "\"failingAtoms\":[]},"
+ + "\"overrideExpressionResult\":{\"value\":{"
+ + "\"expression\":{\"expressionString\":\"label:Override=+1\"},"
+ + "\"status\":\"PASS\",\"errorMessage\":{\"value\":null},"
+ + "\"passingAtoms\":[],"
+ + "\"failingAtoms\":[\"label:Override=+1\"]}},"
+ + "\"patchSetCommitId\":\"4663ab9e9eb49a214e68e60f0fe5d0b6f44f763e\","
+ + "\"legacy\":{\"value\":true},"
+ + "\"forced\":{\"value\":null}}";
+
+ private static final Gson gson = new ChangeNoteJson().getGson();
+
+ @Test
+ public void submitRequirementExpression_serialize() {
+ assertThat(SubmitRequirementExpression.typeAdapter(gson).toJson(srReqExp))
+ .isEqualTo(srReqExpSerial);
+ }
+
+ @Test
+ public void submitRequirementExpression_deserialize() throws Exception {
+ assertThat(SubmitRequirementExpression.typeAdapter(gson).fromJson(srReqExpSerial))
+ .isEqualTo(srReqExp);
+ }
+
+ @Test
+ public void submitRequirementExpression_roundTrip() throws Exception {
+ SubmitRequirementExpression exp = SubmitRequirementExpression.create("label:Code-Review=+2");
+ TypeAdapter<SubmitRequirementExpression> adapter =
+ SubmitRequirementExpression.typeAdapter(gson);
+ assertThat(adapter.fromJson(adapter.toJson(exp))).isEqualTo(exp);
+ }
+
+ @Test
+ public void submitRequirement_serialize() throws Exception {
+ assertThat(SubmitRequirement.typeAdapter(gson).toJson(sr)).isEqualTo(srSerial);
+ }
+
+ @Test
+ public void submitRequirement_deserialize() throws Exception {
+ assertThat(SubmitRequirement.typeAdapter(gson).fromJson(srSerial)).isEqualTo(sr);
+ }
+
+ @Test
+ public void submitRequirement_roundTrip() throws Exception {
+ TypeAdapter<SubmitRequirement> adapter = SubmitRequirement.typeAdapter(gson);
+ assertThat(adapter.fromJson(adapter.toJson(sr))).isEqualTo(sr);
+ }
+
+ @Test
+ public void submitRequirementExpressionResult_serialize() throws Exception {
+ assertThat(SubmitRequirementExpressionResult.typeAdapter(gson).toJson(srExpResult))
+ .isEqualTo(srExpResultSerial);
+ }
+
+ @Test
+ public void submitRequirementExpressionResult_deserialize() throws Exception {
+ assertThat(SubmitRequirementExpressionResult.typeAdapter(gson).fromJson(srExpResultSerial))
+ .isEqualTo(srExpResult);
+ }
+
+ @Test
+ public void submitRequirementExpressionResult_roundtrip() throws Exception {
+ TypeAdapter<SubmitRequirementExpressionResult> adapter =
+ SubmitRequirementExpressionResult.typeAdapter(gson);
+ assertThat(adapter.fromJson(adapter.toJson(srExpResult))).isEqualTo(srExpResult);
+ }
+
+ @Test
+ public void submitRequirementResult_serialize() throws Exception {
+ assertThat(SubmitRequirementResult.typeAdapter(gson).toJson(srReqResult))
+ .isEqualTo(srReqResultSerial);
+ }
+
+ @Test
+ public void submitRequirementResult_deserialize() throws Exception {
+ assertThat(SubmitRequirementResult.typeAdapter(gson).fromJson(srReqResultSerial))
+ .isEqualTo(srReqResult);
+ }
+
+ @Test
+ public void submitRequirementResult_roundTrip() throws Exception {
+ TypeAdapter<SubmitRequirementResult> adapter = SubmitRequirementResult.typeAdapter(gson);
+ assertThat(adapter.fromJson(adapter.toJson(srReqResult))).isEqualTo(srReqResult);
+ }
+
+ @Test
+ public void deserializeSubmitRequirementResult_withJGitPatchsetIdFormat() throws Exception {
+ String srResultSerialJgitFormat =
+ srReqResultSerial.replace(
+ "\"4663ab9e9eb49a214e68e60f0fe5d0b6f44f763e\"",
+ "{\"w1\":1180937118,\"w2\":-1632331231,\"w3\":1315497487,"
+ + "\"w4\":266719414,\"w5\":-196118978}");
+ assertThat(SubmitRequirementResult.typeAdapter(gson).fromJson(srResultSerialJgitFormat))
+ .isEqualTo(srReqResult);
+ }
+
+ @Test
+ public void submitRequirementResult_deserializeNonOptionalLegacyField() throws Exception {
+ String srResultSerialMandatoryLegacyFieldFormat =
+ srReqResultSerial.replace("\"legacy\":{\"value\":true}", "\"legacy\":true");
+ assertThat(
+ SubmitRequirementResult.typeAdapter(gson)
+ .fromJson(srResultSerialMandatoryLegacyFieldFormat))
+ .isEqualTo(srReqResult);
+ }
+
+ @Test
+ public void submitRequirementResult_emptyLegacyField_roundTrip() throws Exception {
+ SubmitRequirementResult srResult = srReqResult.toBuilder().legacy(Optional.empty()).build();
+ TypeAdapter<SubmitRequirementResult> adapter = SubmitRequirementResult.typeAdapter(gson);
+ assertThat(adapter.fromJson(adapter.toJson(srResult))).isEqualTo(srResult);
+ }
+}
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementSerializerTest.java
index a1dee1a..6993dfe 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/SubmitRequirementSerializerTest.java
@@ -26,7 +26,7 @@
public class SubmitRequirementSerializerTest {
private static final SubmitRequirement submitReq =
SubmitRequirement.builder()
- .setName("code-review")
+ .setName("Code-Review")
.setDescription(Optional.of("require code review +2"))
.setApplicabilityExpression(SubmitRequirementExpression.of("branch(refs/heads/master)"))
.setSubmittabilityExpression(SubmitRequirementExpression.create("label(code-review, 2+)"))
diff --git a/javatests/com/google/gerrit/server/change/CommentThreadTest.java b/javatests/com/google/gerrit/server/change/CommentThreadTest.java
index 08485a4..d0b6c14 100644
--- a/javatests/com/google/gerrit/server/change/CommentThreadTest.java
+++ b/javatests/com/google/gerrit/server/change/CommentThreadTest.java
@@ -20,7 +20,7 @@
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.HumanComment;
-import java.sql.Timestamp;
+import java.time.Instant;
import org.junit.Test;
public class CommentThreadTest {
@@ -60,7 +60,7 @@
return new HumanComment(
new Comment.Key(commentUuid, "myFile", 1),
Account.id(100),
- new Timestamp(1234),
+ Instant.ofEpochMilli(1234),
(short) 1,
"Comment text",
"serverId",
diff --git a/javatests/com/google/gerrit/server/change/CommentThreadsTest.java b/javatests/com/google/gerrit/server/change/CommentThreadsTest.java
index 0c61906..83e8370 100644
--- a/javatests/com/google/gerrit/server/change/CommentThreadsTest.java
+++ b/javatests/com/google/gerrit/server/change/CommentThreadsTest.java
@@ -21,7 +21,7 @@
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.HumanComment;
-import java.sql.Timestamp;
+import java.time.Instant;
import org.junit.Test;
public class CommentThreadsTest {
@@ -126,13 +126,15 @@
@Test
public void branchedThreadsAreFlattenedAccordingToDate() {
- HumanComment root = writtenOn(createComment("root"), new Timestamp(1));
- HumanComment sibling1 = writtenOn(asReply(createComment("sibling1"), "root"), new Timestamp(2));
- HumanComment sibling2 = writtenOn(asReply(createComment("sibling2"), "root"), new Timestamp(3));
+ HumanComment root = writtenOn(createComment("root"), Instant.ofEpochMilli(1));
+ HumanComment sibling1 =
+ writtenOn(asReply(createComment("sibling1"), "root"), Instant.ofEpochMilli(2));
+ HumanComment sibling2 =
+ writtenOn(asReply(createComment("sibling2"), "root"), Instant.ofEpochMilli(3));
HumanComment sibling1Child =
- writtenOn(asReply(createComment("sibling1Child"), "sibling1"), new Timestamp(4));
+ writtenOn(asReply(createComment("sibling1Child"), "sibling1"), Instant.ofEpochMilli(4));
HumanComment sibling2Child =
- writtenOn(asReply(createComment("sibling2Child"), "sibling2"), new Timestamp(5));
+ writtenOn(asReply(createComment("sibling2Child"), "sibling2"), Instant.ofEpochMilli(5));
ImmutableList<HumanComment> comments =
ImmutableList.of(sibling2, sibling2Child, sibling1, sibling1Child, root);
@@ -146,9 +148,11 @@
@Test
public void threadsConsiderParentRelationshipStrongerThanDate() {
- HumanComment root = writtenOn(createComment("root"), new Timestamp(3));
- HumanComment child1 = writtenOn(asReply(createComment("child1"), "root"), new Timestamp(2));
- HumanComment child2 = writtenOn(asReply(createComment("child2"), "child1"), new Timestamp(1));
+ HumanComment root = writtenOn(createComment("root"), Instant.ofEpochMilli(3));
+ HumanComment child1 =
+ writtenOn(asReply(createComment("child1"), "root"), Instant.ofEpochMilli(2));
+ HumanComment child2 =
+ writtenOn(asReply(createComment("child2"), "child1"), Instant.ofEpochMilli(1));
ImmutableList<HumanComment> comments = ImmutableList.of(child2, child1, root);
ImmutableSet<CommentThread<HumanComment>> commentThreads =
@@ -161,9 +165,11 @@
@Test
public void threadsFallBackToUuidOrderIfParentAndDateAreTheSame() {
- HumanComment root = writtenOn(createComment("root"), new Timestamp(1));
- HumanComment sibling1 = writtenOn(asReply(createComment("sibling1"), "root"), new Timestamp(2));
- HumanComment sibling2 = writtenOn(asReply(createComment("sibling2"), "root"), new Timestamp(2));
+ HumanComment root = writtenOn(createComment("root"), Instant.ofEpochMilli(1));
+ HumanComment sibling1 =
+ writtenOn(asReply(createComment("sibling1"), "root"), Instant.ofEpochMilli(2));
+ HumanComment sibling2 =
+ writtenOn(asReply(createComment("sibling2"), "root"), Instant.ofEpochMilli(2));
ImmutableList<HumanComment> comments = ImmutableList.of(sibling2, sibling1, root);
ImmutableSet<CommentThread<HumanComment>> commentThreads =
@@ -224,13 +230,15 @@
@Test
public void completeThreadWithBranchesCanBeRequestedByReplyToIntermediateComment() {
- HumanComment root = writtenOn(createComment("root"), new Timestamp(1));
- HumanComment sibling1 = writtenOn(asReply(createComment("sibling1"), "root"), new Timestamp(2));
- HumanComment sibling2 = writtenOn(asReply(createComment("sibling2"), "root"), new Timestamp(3));
+ HumanComment root = writtenOn(createComment("root"), Instant.ofEpochMilli(1));
+ HumanComment sibling1 =
+ writtenOn(asReply(createComment("sibling1"), "root"), Instant.ofEpochMilli(2));
+ HumanComment sibling2 =
+ writtenOn(asReply(createComment("sibling2"), "root"), Instant.ofEpochMilli(3));
HumanComment sibling1Child =
- writtenOn(asReply(createComment("sibling1Child"), "sibling1"), new Timestamp(4));
+ writtenOn(asReply(createComment("sibling1Child"), "sibling1"), Instant.ofEpochMilli(4));
HumanComment sibling2Child =
- writtenOn(asReply(createComment("sibling2Child"), "sibling2"), new Timestamp(5));
+ writtenOn(asReply(createComment("sibling2Child"), "sibling2"), Instant.ofEpochMilli(5));
HumanComment reply = asReply(createComment("sibling1"), "root");
@@ -262,7 +270,7 @@
return new HumanComment(
new Comment.Key(commentUuid, "myFile", 1),
Account.id(100),
- new Timestamp(1234),
+ Instant.ofEpochMilli(1234),
(short) 1,
"Comment text",
"serverId",
@@ -274,8 +282,8 @@
return comment;
}
- private static HumanComment writtenOn(HumanComment comment, Timestamp writtenOn) {
- comment.writtenOn = writtenOn;
+ private static HumanComment writtenOn(HumanComment comment, Instant writtenOn) {
+ comment.setWrittenOn(writtenOn);
return comment;
}
diff --git a/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java b/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
index 8f341aa..38e50b5 100644
--- a/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
+++ b/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
@@ -213,7 +213,7 @@
return PatchSetApproval.builder()
.key(PatchSetApproval.key(change.currentPatchSetId(), accountId, LabelId.create(label)))
.value(value)
- .granted(TimeUtil.nowTs())
+ .granted(TimeUtil.now())
.build();
}
diff --git a/javatests/com/google/gerrit/server/events/EventDeserializerTest.java b/javatests/com/google/gerrit/server/events/EventDeserializerTest.java
index 97f6e4e..00b92b4 100644
--- a/javatests/com/google/gerrit/server/events/EventDeserializerTest.java
+++ b/javatests/com/google/gerrit/server/events/EventDeserializerTest.java
@@ -27,8 +27,8 @@
import com.google.gerrit.server.data.AccountAttribute;
import com.google.gerrit.server.data.ChangeAttribute;
import com.google.gerrit.server.data.RefUpdateAttribute;
+import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gson.Gson;
-import java.sql.Timestamp;
import org.junit.Test;
public class EventDeserializerTest {
@@ -278,7 +278,7 @@
Change.id(1000),
Account.id(1000),
BranchNameKey.create(Project.nameKey("myproject"), "mybranch"),
- new Timestamp(System.currentTimeMillis()));
+ TimeUtil.now());
return change;
}
@@ -335,7 +335,7 @@
a.commitMessage = "This is a test commit message";
a.url = "http://somewhere.com";
a.status = change.getStatus();
- a.createdOn = change.getCreatedOn().getTime() / 1000L;
+ a.createdOn = change.getCreatedOn().getEpochSecond();
a.wip = change.isWorkInProgress() ? true : null;
a.isPrivate = change.isPrivate() ? true : null;
return Suppliers.ofInstance(a);
diff --git a/javatests/com/google/gerrit/server/events/EventJsonTest.java b/javatests/com/google/gerrit/server/events/EventJsonTest.java
index 8e4f436..3c9a355 100644
--- a/javatests/com/google/gerrit/server/events/EventJsonTest.java
+++ b/javatests/com/google/gerrit/server/events/EventJsonTest.java
@@ -607,7 +607,7 @@
Change.id(CHANGE_NUM),
Account.id(9999),
BranchNameKey.create(Project.nameKey(PROJECT), BRANCH),
- TimeUtil.nowTs());
+ TimeUtil.now());
}
private <T> Supplier<T> createSupplier(T value) {
@@ -625,7 +625,7 @@
a.commitMessage = COMMIT_MESSAGE;
a.url = URL;
a.status = change.getStatus();
- a.createdOn = change.getCreatedOn().getTime() / 1000L;
+ a.createdOn = change.getCreatedOn().getEpochSecond();
a.wip = change.isWorkInProgress() ? true : null;
a.isPrivate = change.isPrivate() ? true : null;
return Suppliers.ofInstance(a);
diff --git a/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java b/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
index 3a8d7e4..29dbe58 100644
--- a/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
+++ b/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
@@ -29,6 +29,7 @@
import com.google.gerrit.testing.InMemoryRepositoryManager;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Date;
import java.util.List;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.CommitBuilder;
@@ -203,11 +204,14 @@
return repo.exactRef(refName);
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
private static ObjectId createCommit(Repository repo, ObjectId treeId, ObjectId parentCommit)
throws IOException {
try (ObjectInserter oi = repo.newObjectInserter()) {
PersonIdent committer =
- new PersonIdent(new PersonIdent("Foo Bar", "foo.bar@baz.com"), TimeUtil.nowTs());
+ new PersonIdent(new PersonIdent("Foo Bar", "foo.bar@baz.com"), Date.from(TimeUtil.now()));
CommitBuilder cb = new CommitBuilder();
cb.setTreeId(treeId);
cb.setCommitter(committer);
diff --git a/javatests/com/google/gerrit/server/git/GitRepositoryManagerTest.java b/javatests/com/google/gerrit/server/git/GitRepositoryManagerTest.java
index 6b8177e..82cc049 100644
--- a/javatests/com/google/gerrit/server/git/GitRepositoryManagerTest.java
+++ b/javatests/com/google/gerrit/server/git/GitRepositoryManagerTest.java
@@ -16,7 +16,7 @@
import static com.google.common.truth.Truth.assertThat;
import com.google.gerrit.entities.Project.NameKey;
-import java.util.SortedSet;
+import java.util.NavigableSet;
import org.eclipse.jgit.lib.Repository;
import org.junit.Before;
import org.junit.Test;
@@ -53,7 +53,7 @@
}
@Override
- public SortedSet<NameKey> list() {
+ public NavigableSet<NameKey> list() {
throw new UnsupportedOperationException("Not implemented");
}
}
diff --git a/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java b/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
index 4902830..6c771d7 100644
--- a/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
+++ b/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
@@ -26,7 +26,7 @@
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.util.SortedSet;
+import java.util.NavigableSet;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
@@ -77,7 +77,7 @@
assertThat(repoManager.getBasePath(someProjectKey).toAbsolutePath().toString())
.isEqualTo(repoManager.getBasePath(someProjectKey).toAbsolutePath().toString());
- SortedSet<Project.NameKey> repoList = repoManager.list();
+ NavigableSet<Project.NameKey> repoList = repoManager.list();
assertThat(repoList).hasSize(1);
assertThat(repoList.toArray(new Project.NameKey[repoList.size()]))
.isEqualTo(new Project.NameKey[] {someProjectKey});
@@ -105,7 +105,7 @@
assertThat(repoManager.getBasePath(someProjectKey).toAbsolutePath().toString())
.isEqualTo(alternateBasePath.toString());
- SortedSet<Project.NameKey> repoList = repoManager.list();
+ NavigableSet<Project.NameKey> repoList = repoManager.list();
assertThat(repoList).hasSize(1);
assertThat(repoList.toArray(new Project.NameKey[repoList.size()]))
.isEqualTo(new Project.NameKey[] {someProjectKey});
@@ -131,7 +131,7 @@
createRepository(repoManager.getBasePath(basePathProject), misplacedProject2);
createRepository(alternateBasePath, misplacedProject1);
- SortedSet<Project.NameKey> repoList = repoManager.list();
+ NavigableSet<Project.NameKey> repoList = repoManager.list();
assertThat(repoList).hasSize(2);
assertThat(repoList.toArray(new Project.NameKey[repoList.size()]))
.isEqualTo(new Project.NameKey[] {altPathProject, basePathProject});
diff --git a/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java b/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
index 690a5cc..6792703 100644
--- a/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
+++ b/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
@@ -30,6 +30,7 @@
import com.google.gerrit.testing.TestTimeUtil;
import java.io.IOException;
import java.util.Arrays;
+import java.util.Date;
import java.util.Optional;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
@@ -220,9 +221,13 @@
return u;
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
private CommitBuilder newCommitBuilder() {
CommitBuilder cb = new CommitBuilder();
- PersonIdent author = new PersonIdent("J. Author", "author@example.com", TimeUtil.nowTs(), TZ);
+ PersonIdent author =
+ new PersonIdent("J. Author", "author@example.com", Date.from(TimeUtil.now()), TZ);
cb.setAuthor(author);
cb.setCommitter(
new PersonIdent(
diff --git a/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
index e24d481..54407ca 100644
--- a/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
+++ b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
@@ -30,7 +30,8 @@
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.testing.InMemoryRepositoryManager;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.Date;
import java.util.Optional;
import java.util.TimeZone;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -59,13 +60,16 @@
protected Account.Id userId;
protected PersonIdent userIdent;
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Before
public void abstractGroupTestSetUp() throws Exception {
allUsersName = new AllUsersName(AllUsersNameProvider.DEFAULT);
repoManager = new InMemoryRepositoryManager();
allUsersRepo = repoManager.createRepository(allUsersName);
serverAccountId = Account.id(SERVER_ACCOUNT_NUMBER);
- serverIdent = new PersonIdent(SERVER_NAME, SERVER_EMAIL, TimeUtil.nowTs(), TZ);
+ serverIdent = new PersonIdent(SERVER_NAME, SERVER_EMAIL, Date.from(TimeUtil.now()), TZ);
userId = Account.id(USER_ACCOUNT_NUMBER);
userIdent = newPersonIdent(userId, serverIdent);
}
@@ -75,12 +79,15 @@
allUsersRepo.close();
}
- protected Timestamp getTipTimestamp(AccountGroup.UUID uuid) throws Exception {
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
+ protected Instant getTipTimestamp(AccountGroup.UUID uuid) throws Exception {
try (RevWalk rw = new RevWalk(allUsersRepo)) {
Ref ref = allUsersRepo.exactRef(RefNames.refsGroups(uuid));
return ref == null
? null
- : new Timestamp(rw.parseCommit(ref.getObjectId()).getAuthorIdent().getWhen().getTime());
+ : rw.parseCommit(ref.getObjectId()).getAuthorIdent().getWhen().toInstant();
}
}
@@ -109,8 +116,11 @@
return md;
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
protected static PersonIdent newPersonIdent() {
- return new PersonIdent(SERVER_NAME, SERVER_EMAIL, TimeUtil.nowTs(), TZ);
+ return new PersonIdent(SERVER_NAME, SERVER_EMAIL, Date.from(TimeUtil.now()), TZ);
}
protected static PersonIdent newPersonIdent(Account.Id id, PersonIdent ident) {
@@ -123,7 +133,7 @@
}
private static Optional<Account> getAccount(Account.Id id) {
- Account.Builder account = Account.builder(id, TimeUtil.nowTs());
+ Account.Builder account = Account.builder(id, TimeUtil.now());
account.setFullName("Account " + id);
return Optional.of(account.build());
}
diff --git a/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java b/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
index 6ad899e..a764654 100644
--- a/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
+++ b/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
@@ -24,7 +24,7 @@
import com.google.gerrit.entities.AccountGroupMemberAudit;
import com.google.gerrit.entities.InternalGroup;
import com.google.gerrit.server.account.GroupUuid;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Set;
import org.eclipse.jgit.lib.PersonIdent;
import org.junit.Before;
@@ -299,7 +299,7 @@
}
private static AccountGroupMemberAudit createExpMemberAudit(
- AccountGroup.Id groupId, Account.Id id, Account.Id addedBy, Timestamp addedOn) {
+ AccountGroup.Id groupId, Account.Id id, Account.Id addedBy, Instant addedOn) {
return AccountGroupMemberAudit.builder()
.groupId(groupId)
.memberId(id)
@@ -309,7 +309,7 @@
}
private static AccountGroupByIdAudit createExpGroupAudit(
- AccountGroup.Id groupId, AccountGroup.UUID uuid, Account.Id addedBy, Timestamp addedOn) {
+ AccountGroup.Id groupId, AccountGroup.UUID uuid, Account.Id addedBy, Instant addedOn) {
return AccountGroupByIdAudit.builder()
.groupId(groupId)
.includeUuid(uuid)
diff --git a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
index 5d88a5f..dbe255c 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
@@ -37,11 +37,12 @@
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.truth.OptionalSubject;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneId;
+import java.util.Date;
import java.util.Optional;
import java.util.TimeZone;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -245,7 +246,7 @@
@Test
public void createdOnDefaultsToNow() throws Exception {
// Git timestamps are only precise to the second.
- Timestamp testStart = TimeUtil.truncateToSecond(TimeUtil.nowTs());
+ Instant testStart = TimeUtil.truncateToSecond(TimeUtil.now());
InternalGroupCreation groupCreation =
InternalGroupCreation.builder()
@@ -261,7 +262,7 @@
@Test
public void specifiedCreatedOnIsRespectedForNewGroup() throws Exception {
- Timestamp createdOn = toTimestamp(LocalDate.of(2017, Month.DECEMBER, 11).atTime(13, 44, 10));
+ Instant createdOn = toInstant(LocalDate.of(2017, Month.DECEMBER, 11).atTime(13, 44, 10));
InternalGroupCreation groupCreation = getPrefilledGroupCreationBuilder().build();
GroupDelta groupDelta = GroupDelta.builder().setUpdatedOn(createdOn).build();
@@ -604,8 +605,8 @@
@Test
public void createdOnIsNotAffectedByFurtherUpdates() throws Exception {
- Timestamp createdOn = toTimestamp(LocalDate.of(2017, Month.MAY, 11).atTime(13, 44, 10));
- Timestamp updatedOn = toTimestamp(LocalDate.of(2017, Month.DECEMBER, 12).atTime(10, 21, 49));
+ Instant createdOn = toInstant(LocalDate.of(2017, Month.MAY, 11).atTime(13, 44, 10));
+ Instant updatedOn = toInstant(LocalDate.of(2017, Month.DECEMBER, 12).atTime(10, 21, 49));
InternalGroupCreation groupCreation = getPrefilledGroupCreationBuilder().build();
GroupDelta initialGroupDelta = GroupDelta.builder().setUpdatedOn(createdOn).build();
@@ -737,7 +738,7 @@
.setOwnerGroupUUID(AccountGroup.uuid("another owner"))
.setVisibleToAll(true)
.setName(AccountGroup.nameKey("Another name"))
- .setUpdatedOn(new Timestamp(92900892))
+ .setUpdatedOn(Instant.ofEpochMilli(92900892))
.setMemberModification(members -> ImmutableSet.of(Account.id(1), Account.id(2)))
.setSubgroupModification(subgroups -> ImmutableSet.of(AccountGroup.uuid("subgroup")))
.build();
@@ -758,7 +759,7 @@
.setOwnerGroupUUID(AccountGroup.uuid("another owner"))
.setVisibleToAll(true)
.setName(AccountGroup.nameKey("Another name"))
- .setUpdatedOn(new Timestamp(92900892))
+ .setUpdatedOn(Instant.ofEpochMilli(92900892))
.setMemberModification(members -> ImmutableSet.of(Account.id(1), Account.id(2)))
.setSubgroupModification(subgroups -> ImmutableSet.of(AccountGroup.uuid("subgroup")))
.build();
@@ -780,7 +781,7 @@
.setOwnerGroupUUID(AccountGroup.uuid("another owner"))
.setVisibleToAll(true)
.setName(AccountGroup.nameKey("Another name"))
- .setUpdatedOn(new Timestamp(92900892))
+ .setUpdatedOn(Instant.ofEpochMilli(92900892))
.setMemberModification(members -> ImmutableSet.of(Account.id(1), Account.id(2)))
.setSubgroupModification(subgroups -> ImmutableSet.of(AccountGroup.uuid("subgroup")))
.build();
@@ -864,7 +865,7 @@
public void newCommitIsNotCreatedForPureUpdatedOnUpdate() throws Exception {
createArbitraryGroup(groupUuid);
- Timestamp updatedOn = toTimestamp(LocalDate.of(3017, Month.DECEMBER, 12).atTime(10, 21, 49));
+ Instant updatedOn = toInstant(LocalDate.of(3017, Month.DECEMBER, 12).atTime(10, 21, 49));
GroupDelta groupDelta = GroupDelta.builder().setUpdatedOn(updatedOn).build();
RevCommit commitBeforeUpdate = getLatestCommitForGroup(groupUuid);
@@ -1005,7 +1006,7 @@
@Test
public void commitTimeMatchesDefaultCreatedOnOfNewGroup() throws Exception {
// Git timestamps are only precise to the second.
- long testStartAsSecondsSinceEpoch = TimeUtil.nowTs().getTime() / 1000;
+ long testStartAsSecondsSinceEpoch = TimeUtil.now().getEpochSecond();
InternalGroupCreation groupCreation =
InternalGroupCreation.builder()
@@ -1032,7 +1033,7 @@
.build();
GroupDelta groupDelta =
GroupDelta.builder()
- .setUpdatedOn(new Timestamp(createdOnAsSecondsSinceEpoch * 1000))
+ .setUpdatedOn(Instant.ofEpochSecond(createdOnAsSecondsSinceEpoch))
.build();
createGroup(groupCreation, groupDelta);
@@ -1040,11 +1041,14 @@
assertThat(revCommit.getCommitTime()).isEqualTo(createdOnAsSecondsSinceEpoch);
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Test
public void timestampOfCommitterMatchesSpecifiedCreatedOnOfNewGroup() throws Exception {
- Timestamp committerTimestamp =
- toTimestamp(LocalDate.of(2017, Month.DECEMBER, 13).atTime(15, 5, 27));
- Timestamp createdOn = toTimestamp(LocalDate.of(2016, Month.MARCH, 11).atTime(23, 49, 11));
+ Instant committerTimestamp =
+ toInstant(LocalDate.of(2017, Month.DECEMBER, 13).atTime(15, 5, 27));
+ Instant createdOn = toInstant(LocalDate.of(2016, Month.MARCH, 11).atTime(23, 49, 11));
InternalGroupCreation groupCreation =
InternalGroupCreation.builder()
@@ -1061,23 +1065,27 @@
groupConfig.setGroupDelta(groupDelta, auditLogFormatter);
PersonIdent committerIdent =
- new PersonIdent("Jane", "Jane@gerritcodereview.com", committerTimestamp, timeZone);
+ new PersonIdent(
+ "Jane", "Jane@gerritcodereview.com", Date.from(committerTimestamp), timeZone);
try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
metaDataUpdate.getCommitBuilder().setCommitter(committerIdent);
groupConfig.commit(metaDataUpdate);
}
RevCommit revCommit = getLatestCommitForGroup(groupUuid);
- assertThat(revCommit.getCommitterIdent().getWhen()).isEqualTo(createdOn);
+ assertThat(revCommit.getCommitterIdent().getWhen().getTime())
+ .isEqualTo(createdOn.toEpochMilli());
assertThat(revCommit.getCommitterIdent().getTimeZone().getRawOffset())
.isEqualTo(timeZone.getRawOffset());
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Test
public void timestampOfAuthorMatchesSpecifiedCreatedOnOfNewGroup() throws Exception {
- Timestamp authorTimestamp =
- toTimestamp(LocalDate.of(2017, Month.DECEMBER, 13).atTime(15, 5, 27));
- Timestamp createdOn = toTimestamp(LocalDate.of(2016, Month.MARCH, 11).atTime(23, 49, 11));
+ Instant authorTimestamp = toInstant(LocalDate.of(2017, Month.DECEMBER, 13).atTime(15, 5, 27));
+ Instant createdOn = toInstant(LocalDate.of(2016, Month.MARCH, 11).atTime(23, 49, 11));
InternalGroupCreation groupCreation =
InternalGroupCreation.builder()
@@ -1094,14 +1102,14 @@
groupConfig.setGroupDelta(groupDelta, auditLogFormatter);
PersonIdent authorIdent =
- new PersonIdent("Jane", "Jane@gerritcodereview.com", authorTimestamp, timeZone);
+ new PersonIdent("Jane", "Jane@gerritcodereview.com", Date.from(authorTimestamp), timeZone);
try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
metaDataUpdate.getCommitBuilder().setAuthor(authorIdent);
groupConfig.commit(metaDataUpdate);
}
RevCommit revCommit = getLatestCommitForGroup(groupUuid);
- assertThat(revCommit.getAuthorIdent().getWhen()).isEqualTo(createdOn);
+ assertThat(revCommit.getAuthorIdent().getWhen().getTime()).isEqualTo(createdOn.toEpochMilli());
assertThat(revCommit.getAuthorIdent().getTimeZone().getRawOffset())
.isEqualTo(timeZone.getRawOffset());
}
@@ -1109,7 +1117,7 @@
@Test
public void commitTimeMatchesDefaultUpdatedOnOfUpdatedGroup() throws Exception {
// Git timestamps are only precise to the second.
- long testStartAsSecondsSinceEpoch = TimeUtil.nowTs().getTime() / 1000;
+ long testStartAsSecondsSinceEpoch = TimeUtil.now().getEpochSecond();
createArbitraryGroup(groupUuid);
GroupDelta groupDelta =
@@ -1129,7 +1137,7 @@
GroupDelta groupDelta =
GroupDelta.builder()
.setName(AccountGroup.nameKey("Another name"))
- .setUpdatedOn(new Timestamp(updatedOnAsSecondsSinceEpoch * 1000))
+ .setUpdatedOn(Instant.ofEpochSecond(updatedOnAsSecondsSinceEpoch))
.build();
updateGroup(groupUuid, groupDelta);
@@ -1138,10 +1146,13 @@
}
@Test
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
public void timestampOfCommitterMatchesSpecifiedUpdatedOnOfUpdatedGroup() throws Exception {
- Timestamp committerTimestamp =
- toTimestamp(LocalDate.of(2017, Month.DECEMBER, 13).atTime(15, 5, 27));
- Timestamp updatedOn = toTimestamp(LocalDate.of(2016, Month.MARCH, 11).atTime(23, 49, 11));
+ Instant committerTimestamp =
+ toInstant(LocalDate.of(2017, Month.DECEMBER, 13).atTime(15, 5, 27));
+ Instant updatedOn = toInstant(LocalDate.of(2016, Month.MARCH, 11).atTime(23, 49, 11));
createArbitraryGroup(groupUuid);
GroupDelta groupDelta =
@@ -1153,23 +1164,27 @@
groupConfig.setGroupDelta(groupDelta, auditLogFormatter);
PersonIdent committerIdent =
- new PersonIdent("Jane", "Jane@gerritcodereview.com", committerTimestamp, timeZone);
+ new PersonIdent(
+ "Jane", "Jane@gerritcodereview.com", Date.from(committerTimestamp), timeZone);
try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
metaDataUpdate.getCommitBuilder().setCommitter(committerIdent);
groupConfig.commit(metaDataUpdate);
}
RevCommit revCommit = getLatestCommitForGroup(groupUuid);
- assertThat(revCommit.getCommitterIdent().getWhen()).isEqualTo(updatedOn);
+ assertThat(revCommit.getCommitterIdent().getWhen().getTime())
+ .isEqualTo(updatedOn.toEpochMilli());
assertThat(revCommit.getCommitterIdent().getTimeZone().getRawOffset())
.isEqualTo(timeZone.getRawOffset());
}
@Test
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
public void timestampOfAuthorMatchesSpecifiedUpdatedOnOfUpdatedGroup() throws Exception {
- Timestamp authorTimestamp =
- toTimestamp(LocalDate.of(2017, Month.DECEMBER, 13).atTime(15, 5, 27));
- Timestamp updatedOn = toTimestamp(LocalDate.of(2016, Month.MARCH, 11).atTime(23, 49, 11));
+ Instant authorTimestamp = toInstant(LocalDate.of(2017, Month.DECEMBER, 13).atTime(15, 5, 27));
+ Instant updatedOn = toInstant(LocalDate.of(2016, Month.MARCH, 11).atTime(23, 49, 11));
createArbitraryGroup(groupUuid);
GroupDelta groupDelta =
@@ -1181,14 +1196,14 @@
groupConfig.setGroupDelta(groupDelta, auditLogFormatter);
PersonIdent authorIdent =
- new PersonIdent("Jane", "Jane@gerritcodereview.com", authorTimestamp, timeZone);
+ new PersonIdent("Jane", "Jane@gerritcodereview.com", Date.from(authorTimestamp), timeZone);
try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
metaDataUpdate.getCommitBuilder().setAuthor(authorIdent);
groupConfig.commit(metaDataUpdate);
}
RevCommit revCommit = getLatestCommitForGroup(groupUuid);
- assertThat(revCommit.getAuthorIdent().getWhen()).isEqualTo(updatedOn);
+ assertThat(revCommit.getAuthorIdent().getWhen().getTime()).isEqualTo(updatedOn.toEpochMilli());
assertThat(revCommit.getAuthorIdent().getTimeZone().getRawOffset())
.isEqualTo(timeZone.getRawOffset());
}
@@ -1455,8 +1470,8 @@
+ "Rename from Old name to New name");
}
- private static Timestamp toTimestamp(LocalDateTime localDateTime) {
- return Timestamp.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
+ private static Instant toInstant(LocalDateTime localDateTime) {
+ return localDateTime.atZone(ZoneId.systemDefault()).toInstant();
}
private void populateGroupConfig(AccountGroup.UUID uuid, String fileContent) throws Exception {
@@ -1539,10 +1554,13 @@
}
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
private MetaDataUpdate createMetaDataUpdate() {
PersonIdent serverIdent =
new PersonIdent(
- "Gerrit Server", "noreply@gerritcodereview.com", TimeUtil.nowTs(), timeZone);
+ "Gerrit Server", "noreply@gerritcodereview.com", Date.from(TimeUtil.now()), timeZone);
MetaDataUpdate metaDataUpdate =
new MetaDataUpdate(
@@ -1564,7 +1582,7 @@
}
private static Account createAccount(Account.Id id, String name) {
- Account.Builder account = Account.builder(id, TimeUtil.nowTs());
+ Account.Builder account = Account.builder(id, TimeUtil.now());
account.setFullName(name);
return account.build();
}
diff --git a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
index 3b7beb9..afc56ff 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
@@ -45,6 +45,7 @@
import com.google.gerrit.truth.OptionalSubject;
import java.io.IOException;
import java.util.Arrays;
+import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.TimeZone;
@@ -557,8 +558,11 @@
return GroupReference.create(AccountGroup.uuid(name + "-" + id), name);
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
private static PersonIdent newPersonIdent() {
- return new PersonIdent(SERVER_NAME, SERVER_EMAIL, TimeUtil.nowTs(), TZ);
+ return new PersonIdent(SERVER_NAME, SERVER_EMAIL, Date.from(TimeUtil.now()), TZ);
}
private static ObjectId getNoteKey(GroupReference g) {
diff --git a/javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java b/javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java
index 9025691..6745b1d 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupsNoteDbConsistencyCheckerTest.java
@@ -17,6 +17,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo.warning;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
@@ -95,7 +96,7 @@
@Test
public void groupNameNoteFailToParse() throws Exception {
updateGroupNamesRef("g-1", "[invalid");
- List<ConsistencyProblemInfo> problems =
+ ImmutableList<ConsistencyProblemInfo> problems =
GroupsNoteDbConsistencyChecker.checkWithGroupNameNotes(
allUsersRepo, AccountGroup.nameKey("g-1"), AccountGroup.uuid("uuid-1"));
assertThat(problems)
diff --git a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
index d16efc3..4d9cb76 100644
--- a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
@@ -36,7 +36,7 @@
@Test
public void refStateFieldValues() throws Exception {
AllUsersName allUsersName = new AllUsersName(AllUsersNameProvider.DEFAULT);
- Account.Builder account = Account.builder(Account.id(1), TimeUtil.nowTs());
+ Account.Builder account = Account.builder(Account.id(1), TimeUtil.now());
String metaId = "0e39795bb25dc914118224995c53c5c36923a461";
account.setMetaId(metaId);
List<String> values =
@@ -51,7 +51,7 @@
public void externalIdStateFieldValues() throws Exception {
Account.Id id = Account.id(1);
Account account =
- Account.builder(id, TimeUtil.nowTs())
+ Account.builder(id, TimeUtil.now())
.setMetaId("1234567812345678123456781234567812345678")
.build();
ExternalId extId1 =
diff --git a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
index 6ad2060..dc1440b 100644
--- a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
@@ -33,7 +33,7 @@
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.testing.TestTimeUtil;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -55,17 +55,22 @@
@Test
public void reviewerFieldValues() {
- Table<ReviewerStateInternal, Account.Id, Timestamp> t = HashBasedTable.create();
- Timestamp t1 = TimeUtil.nowTs();
+ Table<ReviewerStateInternal, Account.Id, Instant> t = HashBasedTable.create();
+
+ // Timestamps are stored as epoch millis in the reviewer field. Epoch millis are less precise
+ // than Instants which have nanosecond precision. Create Instants with millisecond precision
+ // here so that the comparison for the assertions works.
+ Instant t1 = Instant.ofEpochMilli(TimeUtil.nowMs());
+ Instant t2 = Instant.ofEpochMilli(TimeUtil.nowMs());
+
t.put(ReviewerStateInternal.REVIEWER, Account.id(1), t1);
- Timestamp t2 = TimeUtil.nowTs();
t.put(ReviewerStateInternal.CC, Account.id(2), t2);
ReviewerSet reviewers = ReviewerSet.fromTable(t);
List<String> values = ChangeField.getReviewerFieldValues(reviewers);
assertThat(values)
.containsExactly(
- "REVIEWER,1", "REVIEWER,1," + t1.getTime(), "CC,2", "CC,2," + t2.getTime());
+ "REVIEWER,1", "REVIEWER,1," + t1.toEpochMilli(), "CC,2", "CC,2," + t2.toEpochMilli());
assertThat(ChangeField.parseReviewerFieldValues(Change.id(1), values)).isEqualTo(reviewers);
}
diff --git a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
index 66a98e8..e879170 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
@@ -16,6 +16,7 @@
import com.google.gerrit.index.query.OperatorPredicate;
import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryBuilder;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import org.eclipse.jgit.lib.Config;
@@ -25,7 +26,7 @@
public class FakeQueryBuilder extends ChangeQueryBuilder {
FakeQueryBuilder(ChangeIndexCollection indexes) {
super(
- new ChangeQueryBuilder.Definition<>(FakeQueryBuilder.class),
+ new QueryBuilder.Definition<>(FakeQueryBuilder.class),
new ChangeQueryBuilder.Arguments(
null,
null,
@@ -56,6 +57,7 @@
new Config(),
null,
null,
+ null,
null));
}
@@ -70,6 +72,6 @@
}
private Predicate<ChangeData> predicate(String name, String value) {
- return new OperatorPredicate<ChangeData>(name, value) {};
+ return new OperatorPredicate<>(name, value) {};
}
}
diff --git a/javatests/com/google/gerrit/server/mail/SignedTokenTest.java b/javatests/com/google/gerrit/server/mail/SignedTokenTest.java
index 41d8d69..27c4f56 100644
--- a/javatests/com/google/gerrit/server/mail/SignedTokenTest.java
+++ b/javatests/com/google/gerrit/server/mail/SignedTokenTest.java
@@ -152,7 +152,7 @@
private static String randomString(int length) {
String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random = new Random();
- StringBuffer sb = new StringBuffer();
+ StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
int number = random.nextInt(62);
sb.append(str.charAt(number));
diff --git a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
index 5980071..629b0cc 100644
--- a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
@@ -362,7 +362,7 @@
private AccountState makeUser(String name, String email) {
final Account.Id userId = Account.id(42);
- final Account.Builder account = Account.builder(userId, TimeUtil.nowTs());
+ final Account.Builder account = Account.builder(userId, TimeUtil.now());
account.setFullName(name);
account.setPreferredEmail(email);
return AccountState.forAccount(account.build());
diff --git a/javatests/com/google/gerrit/server/mail/send/MailSoySauceProviderTest.java b/javatests/com/google/gerrit/server/mail/send/MailSoySauceLoaderTest.java
similarity index 89%
rename from javatests/com/google/gerrit/server/mail/send/MailSoySauceProviderTest.java
rename to javatests/com/google/gerrit/server/mail/send/MailSoySauceLoaderTest.java
index 2ec5e4d..fbeabe1 100644
--- a/javatests/com/google/gerrit/server/mail/send/MailSoySauceProviderTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/MailSoySauceLoaderTest.java
@@ -25,7 +25,7 @@
import org.junit.Before;
import org.junit.Test;
-public class MailSoySauceProviderTest {
+public class MailSoySauceLoaderTest {
private SitePaths sitePaths;
private DynamicSet<MailSoyTemplateProvider> set;
@@ -38,11 +38,11 @@
@Test
public void soyCompilation() {
- MailSoySauceProvider provider =
- new MailSoySauceProvider(
+ MailSoySauceLoader loader =
+ new MailSoySauceLoader(
sitePaths,
new SoyAstCache(),
new PluginSetContext<>(set, PluginMetrics.DISABLED_INSTANCE));
- assertThat(provider.get()).isNotNull(); // should not throw
+ assertThat(loader.load()).isNotNull(); // should not throw
}
}
diff --git a/javatests/com/google/gerrit/server/mail/send/MailSoySauceModuleTest.java b/javatests/com/google/gerrit/server/mail/send/MailSoySauceModuleTest.java
new file mode 100644
index 0000000..bb443f8
--- /dev/null
+++ b/javatests/com/google/gerrit/server/mail/send/MailSoySauceModuleTest.java
@@ -0,0 +1,60 @@
+package com.google.gerrit.server.mail.send;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService;
+
+import com.google.common.cache.LoadingCache;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.metrics.DisabledMetricMaker;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.CacheRefreshExecutor;
+import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Names;
+import com.google.template.soy.jbcsrc.api.SoySauce;
+import java.nio.file.Paths;
+import javax.inject.Provider;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class MailSoySauceModuleTest {
+ @Test
+ public void soySauceProviderReturnsCachedValue() throws Exception {
+ SitePaths sitePaths = new SitePaths(Paths.get("."));
+ Injector injector =
+ Guice.createInjector(
+ new MailSoySauceModule(),
+ new AbstractModule() {
+ @Override
+ protected void configure() {
+ super.configure();
+ bind(ListeningExecutorService.class)
+ .annotatedWith(CacheRefreshExecutor.class)
+ .toInstance(newDirectExecutorService());
+ bind(SitePaths.class).toInstance(sitePaths);
+ bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(new Config());
+ bind(MetricMaker.class).to(DisabledMetricMaker.class);
+ install(new DefaultMemoryCacheModule());
+ }
+ });
+ Provider<SoySauce> soySauceProvider =
+ injector.getProvider(Key.get(SoySauce.class, MailTemplates.class));
+ LoadingCache<String, SoySauce> cache =
+ injector.getInstance(
+ Key.get(
+ new TypeLiteral<LoadingCache<String, SoySauce>>() {},
+ Names.named(MailSoySauceModule.CACHE_NAME)));
+ assertThat(cache.stats().loadCount()).isEqualTo(0);
+ // Theoretically, this can be flaky, if the delay before the second get takes several seconds.
+ // We assume that tests is fast enough.
+ assertThat(soySauceProvider.get()).isNotNull();
+ assertThat(soySauceProvider.get()).isNotNull();
+ assertThat(cache.stats().loadCount()).isEqualTo(1);
+ }
+}
diff --git a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index ba514fd..222be83 100644
--- a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -39,6 +39,8 @@
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.account.ServiceUserClassifier;
+import com.google.gerrit.server.approval.PatchSetApprovalUuidGenerator;
+import com.google.gerrit.server.approval.testing.TestPatchSetApprovalUuidGenerator;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.AllUsersNameProvider;
import com.google.gerrit.server.config.AnonymousCowardName;
@@ -65,7 +67,8 @@
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
-import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.Date;
import java.util.TimeZone;
import java.util.concurrent.ExecutorService;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -112,22 +115,26 @@
protected Injector injector;
private String systemTimeZone;
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Before
public void setUpTestEnvironment() throws Exception {
setTimeForTesting();
- serverIdent = new PersonIdent("Gerrit Server", "noreply@gerrit.com", TimeUtil.nowTs(), TZ);
+ serverIdent =
+ new PersonIdent("Gerrit Server", "noreply@gerrit.com", Date.from(TimeUtil.now()), TZ);
project = Project.nameKey("test-project");
repoManager = new InMemoryRepositoryManager();
repo = repoManager.createRepository(project);
tr = new TestRepository<>(repo);
rw = tr.getRevWalk();
accountCache = new FakeAccountCache();
- Account.Builder co = Account.builder(Account.id(1), TimeUtil.nowTs());
+ Account.Builder co = Account.builder(Account.id(1), TimeUtil.now());
co.setFullName("Change Owner");
co.setPreferredEmail("change@owner.com");
accountCache.put(co.build());
- Account.Builder ou = Account.builder(Account.id(2), TimeUtil.nowTs());
+ Account.Builder ou = Account.builder(Account.id(2), TimeUtil.now());
ou.setFullName("Other Account");
ou.setPreferredEmail("other@account.com");
accountCache.put(ou.build());
@@ -173,6 +180,8 @@
() -> {
throw new UnsupportedOperationException();
});
+ bind(PatchSetApprovalUuidGenerator.class)
+ .to(TestPatchSetApprovalUuidGenerator.class);
}
});
@@ -261,7 +270,7 @@
int line,
IdentifiedUser commenter,
String parentUUID,
- Timestamp t,
+ Instant t,
String message,
short side,
ObjectId commitId,
@@ -282,11 +291,11 @@
return c;
}
- protected static Timestamp truncate(Timestamp ts) {
- return new Timestamp((ts.getTime() / 1000) * 1000);
+ protected static Instant truncate(Instant ts) {
+ return Instant.ofEpochMilli((ts.toEpochMilli() / 1000) * 1000);
}
- protected static Timestamp after(Change c, long millis) {
- return new Timestamp(c.getCreatedOn().getTime() + millis);
+ protected static Instant after(Change c, long millis) {
+ return Instant.ofEpochMilli(c.getCreatedOn().toEpochMilli() + millis);
}
}
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesCommitTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesCommitTest.java
index f105cf1..666b8fc 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesCommitTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesCommitTest.java
@@ -106,7 +106,7 @@
ChangeNotes notes = newNotes(change).load();
ChangeNoteUtil noteUtil = injector.getInstance(ChangeNoteUtil.class);
PersonIdent author =
- noteUtil.newAccountIdIdent(changeOwner.getAccount().id(), TimeUtil.nowTs(), serverIdent);
+ noteUtil.newAccountIdIdent(changeOwner.getAccount().id(), TimeUtil.now(), serverIdent);
try (ObjectInserter ins = testRepo.getRepository().newObjectInserter()) {
CommitBuilder cb = new CommitBuilder();
cb.setParentId(notes.getRevision());
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index 256544c..e557277 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -153,6 +153,42 @@
}
@Test
+ public void parseApprovalWithUUID() throws Exception {
+ assertParseSucceeds(
+ "Update change\n"
+ + "\n"
+ + "Branch: refs/heads/master\n"
+ + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
+ + "Patch-set: 1\n"
+ + "Label: Label1=+1, 577fb248e474018276351785930358ec0450e9f7\n"
+ + "Label: Label1=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 2 <2@gerrit>\n"
+ + "Label: Label1=0, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 2 <2@gerrit>\n"
+ + "Subject: This is a test change\n");
+
+ assertParseSucceeds(
+ "Update change\n"
+ + "\n"
+ + "Branch: refs/heads/master\n"
+ + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
+ + "Patch-set: 1\n"
+ + "Label: Label1=+1, non-SHA1_UUID\n"
+ + "Label: Label1=+1, non-SHA1_UUID Gerrit User 2 <2@gerrit>\n"
+ + "Label: Label1=0, non-SHA1_UUID Gerrit User 2 <2@gerrit>\n"
+ + "Subject: This is a test change\n");
+ assertParseFails("Update change\n\nPatch-set: 1\nLabel: Label1=+1, \n");
+ assertParseFails("Update change\n\nPatch-set: 1\nLabel: Label1=+1,\n");
+ assertParseFails(
+ "Update change\n\nPatch-set: 1\nLabel: Label1=-1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 2 <2@gerrit>\n");
+ assertParseFails(
+ "Update change\n\nPatch-set: 1\nLabel: Label1=-1, 577fb248e474018276351785930358ec0450e9f7\n");
+ // UUID for removals is not supported.
+ assertParseFails(
+ "Update change\n\nPatch-set: 1\nLabel: -Label1, 577fb248e474018276351785930358ec0450e9f7\n");
+ assertParseFails(
+ "Update change\n\nPatch-set: 1\nLabel: -Label1, 577fb248e474018276351785930358ec0450e9f7 Other Account <2@gerrit>\n");
+ }
+
+ @Test
public void parseCopiedApproval() throws Exception {
assertParseSucceeds(
"Update change\n"
@@ -165,15 +201,7 @@
+ "Copied-Label: Label3=+1 Account <1@gerrit>,Other Account <2@Gerrit> :\"tag\"\n"
+ "Copied-Label: Label4=+1 Account <1@Gerrit> :\"tag with characters %^#@^( *::!\"\n"
+ "Subject: This is a test change\n");
- assertParseSucceeds(
- "Update change\n"
- + "\n"
- + "Branch: refs/heads/master\n"
- + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
- + "Patch-set: 1\n"
- + "Label: -Label1\n"
- + "Label: -Label4 Account <1@gerrit>\n"
- + "Subject: This is a test change\n");
+
assertParseFails("Update change\n\nPatch-set: 1\nCopied-Label: Label1=X\n");
assertParseFails("Update change\n\nPatch-set: 1\nCopied-Label: Label1 = 1\n");
assertParseFails("Update change\n\nPatch-set: 1\nCopied-Label: X+Y\n");
@@ -190,6 +218,50 @@
}
@Test
+ public void parseCopiedApprovalWithUUID() throws Exception {
+ assertParseFails("Update change\n\nPatch-set: 1\nCopied-Label: Label1=+1 ,\n");
+ assertParseSucceeds(
+ "Update change\n"
+ + "\n"
+ + "Branch: refs/heads/master\n"
+ + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
+ + "Patch-set: 1\n"
+ + "Copied-Label: Label2=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1 <1@gerrit>\n"
+ + "Copied-Label: Label1=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1 <1@gerrit>,Gerrit User 2 <2@gerrit>\n"
+ + "Copied-Label: Label3=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1 <1@gerrit>,Gerrit User 2 <2@gerrit> :\"tag\"\n"
+ + "Copied-Label: Label4=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1 <1@gerrit> :\"tag with characters %^#@^( *::!\"\n"
+ + "Copied-Label: Label4=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1 <1@gerrit> :\"tag with uuid delimiter , \"\n"
+ + "Copied-Label: Label4=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1 <1@gerrit>,Gerrit User 2 <2@gerrit> :\"tag with characters %^#@^( *::!\"\n"
+ + "Copied-Label: Label4=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1 <1@gerrit>,Gerrit User 2 <2@gerrit> :\"tag with uuid delimiter , \"\n"
+ + "Subject: This is a test change\n");
+
+ assertParseSucceeds(
+ "Update change\n"
+ + "\n"
+ + "Branch: refs/heads/master\n"
+ + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
+ + "Patch-set: 1\n"
+ + "Copied-Label: Label2=+1, non-SHA1_UUID Gerrit User 1 <1@gerrit>\n"
+ + "Copied-Label: Label1=+1, non-SHA1_UUID Gerrit User 1 <1@gerrit>,Gerrit User 2 <2@gerrit>\n"
+ + "Copied-Label: Label3=+1, non-SHA1_UUID Gerrit User 1 <1@gerrit>,Gerrit User 2 <2@gerrit> :\"tag\"\n"
+ + "Copied-Label: Label4=+1, non-SHA1_UUID Gerrit User 1 <1@gerrit> :\"tag with characters %^#@^( *::!\"\n"
+ + "Copied-Label: Label4=+1, non-SHA1_UUID Gerrit User 1 <1@gerrit> :\"tag with uuid delimiter , \"\n"
+ + "Copied-Label: Label4=+1, non-SHA1_UUID Gerrit User 1 <1@gerrit>,Gerrit User 2 <2@gerrit> :\"tag with characters %^#@^( *::!\"\n"
+ + "Copied-Label: Label4=+1, non-SHA1_UUID Gerrit User 1 <1@gerrit>,Gerrit User 2 <2@gerrit> :\"tag with uuid delimiter , \"\n"
+ + "Subject: This is a test change\n");
+
+ assertParseFails("Update change\n\nPatch-set: 1\nCopied-Label: Label1=+1,\n");
+ assertParseFails("Update change\n\nPatch-set: 1\nCopied-Label: Label1=+1,\n");
+ assertParseFails("Update change\n\nPatch-set: 1\nCopied-Label: Label1=+1 ,\n");
+ assertParseFails(
+ "Copied-Label: Label1=+1, 577fb248e474018276351785930358ec0450e9f7 Gerrit User 1 <1@gerrit>,Gerrit User 2 <2@gerrit>\n\n");
+ assertParseFails(
+ "Update change\n\nPatch-set: 1\nCopied-Label: Label1=+1, 577fb248e474018276351785930358ec0450e9f7");
+ assertParseFails(
+ "Update change\n\nPatch-set: 1\nCopied-Label: Label1=+1, 577fb248e474018276351785930358ec0450e9f7 :\"tag\"\n");
+ }
+
+ @Test
public void parseSubmitRecords() throws Exception {
assertParseSucceeds(
"Update change\n"
@@ -640,7 +712,7 @@
ChangeNoteUtil noteUtil = injector.getInstance(ChangeNoteUtil.class);
return writeCommit(
body,
- noteUtil.newAccountIdIdent(changeOwner.getAccount().id(), TimeUtil.nowTs(), serverIdent),
+ noteUtil.newAccountIdIdent(changeOwner.getAccount().id(), TimeUtil.now(), serverIdent),
false);
}
@@ -652,7 +724,7 @@
ChangeNoteUtil noteUtil = injector.getInstance(ChangeNoteUtil.class);
return writeCommit(
body,
- noteUtil.newAccountIdIdent(changeOwner.getAccount().id(), TimeUtil.nowTs(), serverIdent),
+ noteUtil.newAccountIdIdent(changeOwner.getAccount().id(), TimeUtil.now(), serverIdent),
initWorkInProgress);
}
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index 61002f9..3295828 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -92,8 +92,8 @@
cols =
ChangeColumns.builder()
.changeKey(Change.key(CHANGE_KEY))
- .createdOn(new Timestamp(123456L))
- .lastUpdatedOn(new Timestamp(234567L))
+ .createdOn(Instant.ofEpochMilli(123456L))
+ .lastUpdatedOn(Instant.ofEpochMilli(234567L))
.owner(Account.id(1000))
.branch("refs/heads/master")
.subject("Test change")
@@ -135,7 +135,9 @@
@Test
public void serializeCreatedOn() throws Exception {
assertRoundTrip(
- newBuilder().columns(cols.toBuilder().createdOn(new Timestamp(98765L)).build()).build(),
+ newBuilder()
+ .columns(cols.toBuilder().createdOn(Instant.ofEpochMilli(98765L)).build())
+ .build(),
ChangeNotesStateProto.newBuilder()
.setMetaId(SHA_BYTES)
.setChangeId(ID.get())
@@ -146,7 +148,9 @@
@Test
public void serializeLastUpdatedOn() throws Exception {
assertRoundTrip(
- newBuilder().columns(cols.toBuilder().lastUpdatedOn(new Timestamp(98765L)).build()).build(),
+ newBuilder()
+ .columns(cols.toBuilder().lastUpdatedOn(Instant.ofEpochMilli(98765L)).build())
+ .build(),
ChangeNotesStateProto.newBuilder()
.setMetaId(SHA_BYTES)
.setChangeId(ID.get())
@@ -376,7 +380,7 @@
PatchSet.id(ID, 1), Account.id(2001), LabelId.create(LabelId.CODE_REVIEW)))
.value(1)
.tag("tag")
- .granted(new Timestamp(1212L))
+ .granted(Instant.ofEpochMilli(1212L))
.build();
Entities.PatchSetApproval psa1 = PatchSetApprovalProtoConverter.INSTANCE.toProto(a1);
ByteString a1Bytes = Protos.toByteString(psa1);
@@ -389,7 +393,7 @@
.value(-1)
.tag("tag")
.copied(true)
- .granted(new Timestamp(3434L))
+ .granted(Instant.ofEpochMilli(3434L))
.build();
Entities.PatchSetApproval psa2 = PatchSetApprovalProtoConverter.INSTANCE.toProto(a2);
ByteString a2Bytes = Protos.toByteString(psa2);
@@ -410,14 +414,62 @@
}
@Test
+ public void serializeApprovalsWithUUID() throws Exception {
+ PatchSetApproval a1 =
+ PatchSetApproval.builder()
+ .key(
+ PatchSetApproval.key(
+ PatchSet.id(ID, 1), Account.id(2001), LabelId.create(LabelId.CODE_REVIEW)))
+ .uuid(Optional.of(PatchSetApproval.uuid("577fb248e474018276351785930358ec0450e9f7")))
+ .value(1)
+ .tag("tag")
+ .granted(Instant.ofEpochMilli(1212L))
+ .build();
+ Entities.PatchSetApproval psa1 = PatchSetApprovalProtoConverter.INSTANCE.toProto(a1);
+ ByteString a1Bytes = Protos.toByteString(psa1);
+
+ PatchSetApproval a2 =
+ PatchSetApproval.builder()
+ .key(
+ PatchSetApproval.key(
+ PatchSet.id(ID, 1), Account.id(2002), LabelId.create(LabelId.VERIFIED)))
+ .uuid(Optional.of(PatchSetApproval.uuid("577fb248e474018276351785930358ec0450e9f7")))
+ .value(-1)
+ .tag("tag")
+ .copied(true)
+ .granted(Instant.ofEpochMilli(3434L))
+ .build();
+ Entities.PatchSetApproval psa2 = PatchSetApprovalProtoConverter.INSTANCE.toProto(a2);
+ ByteString a2Bytes = Protos.toByteString(psa2);
+ assertThat(a2Bytes.size()).isEqualTo(98);
+ assertThat(a2Bytes).isNotEqualTo(a1Bytes);
+
+ assertRoundTrip(
+ newBuilder()
+ .approvals(ImmutableListMultimap.of(a2.patchSetId(), a2, a1.patchSetId(), a1).entries())
+ .build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto)
+ .addApproval(psa2)
+ .addApproval(psa1)
+ .build());
+ }
+
+ @Test
public void serializeReviewers() throws Exception {
assertRoundTrip(
newBuilder()
.reviewers(
ReviewerSet.fromTable(
- ImmutableTable.<ReviewerStateInternal, Account.Id, Timestamp>builder()
- .put(ReviewerStateInternal.CC, Account.id(2001), new Timestamp(1212L))
- .put(ReviewerStateInternal.REVIEWER, Account.id(2002), new Timestamp(3434L))
+ ImmutableTable.<ReviewerStateInternal, Account.Id, Instant>builder()
+ .put(
+ ReviewerStateInternal.CC, Account.id(2001), Instant.ofEpochMilli(1212L))
+ .put(
+ ReviewerStateInternal.REVIEWER,
+ Account.id(2002),
+ Instant.ofEpochMilli(3434L))
.build()))
.build(),
ChangeNotesStateProto.newBuilder()
@@ -443,15 +495,15 @@
newBuilder()
.reviewersByEmail(
ReviewerByEmailSet.fromTable(
- ImmutableTable.<ReviewerStateInternal, Address, Timestamp>builder()
+ ImmutableTable.<ReviewerStateInternal, Address, Instant>builder()
.put(
ReviewerStateInternal.CC,
Address.create("Name1", "email1@example.com"),
- new Timestamp(1212L))
+ Instant.ofEpochMilli(1212L))
.put(
ReviewerStateInternal.REVIEWER,
Address.create("Name2", "email2@example.com"),
- new Timestamp(3434L))
+ Instant.ofEpochMilli(3434L))
.build()))
.build(),
ChangeNotesStateProto.newBuilder()
@@ -481,7 +533,7 @@
ImmutableTable.of(
ReviewerStateInternal.CC,
Address.create("emailonly@example.com"),
- new Timestamp(1212L))))
+ Instant.ofEpochMilli(1212L))))
.build(),
ChangeNotesStateProto.newBuilder()
.setMetaId(SHA_BYTES)
@@ -509,9 +561,13 @@
newBuilder()
.pendingReviewers(
ReviewerSet.fromTable(
- ImmutableTable.<ReviewerStateInternal, Account.Id, Timestamp>builder()
- .put(ReviewerStateInternal.CC, Account.id(2001), new Timestamp(1212L))
- .put(ReviewerStateInternal.REVIEWER, Account.id(2002), new Timestamp(3434L))
+ ImmutableTable.<ReviewerStateInternal, Account.Id, Instant>builder()
+ .put(
+ ReviewerStateInternal.CC, Account.id(2001), Instant.ofEpochMilli(1212L))
+ .put(
+ ReviewerStateInternal.REVIEWER,
+ Account.id(2002),
+ Instant.ofEpochMilli(3434L))
.build()))
.build(),
ChangeNotesStateProto.newBuilder()
@@ -537,15 +593,15 @@
newBuilder()
.pendingReviewersByEmail(
ReviewerByEmailSet.fromTable(
- ImmutableTable.<ReviewerStateInternal, Address, Timestamp>builder()
+ ImmutableTable.<ReviewerStateInternal, Address, Instant>builder()
.put(
ReviewerStateInternal.CC,
Address.create("Name1", "email1@example.com"),
- new Timestamp(1212L))
+ Instant.ofEpochMilli(1212L))
.put(
ReviewerStateInternal.REVIEWER,
Address.create("Name2", "email2@example.com"),
- new Timestamp(3434L))
+ Instant.ofEpochMilli(3434L))
.build()))
.build(),
ChangeNotesStateProto.newBuilder()
@@ -585,12 +641,12 @@
.reviewerUpdates(
ImmutableList.of(
ReviewerStatusUpdate.create(
- new Timestamp(1212L),
+ Instant.ofEpochMilli(1212L),
Account.id(1000),
Account.id(2002),
ReviewerStateInternal.CC),
ReviewerStatusUpdate.create(
- new Timestamp(3434L),
+ Instant.ofEpochMilli(3434L),
Account.id(1000),
Account.id(2001),
ReviewerStateInternal.REVIEWER)))
@@ -696,7 +752,7 @@
.setApplicabilityExpression(
SubmitRequirementExpression.of("project:foo"))
.setSubmittabilityExpression(
- SubmitRequirementExpression.create("label:code-review=+2"))
+ SubmitRequirementExpression.create("label:Code-Review=+2"))
.setAllowOverrideInChildProjects(false)
.build())
.applicabilityExpressionResult(
@@ -708,10 +764,10 @@
ImmutableList.of())))
.submittabilityExpressionResult(
SubmitRequirementExpressionResult.create(
- SubmitRequirementExpression.create("label:code-review=+2"),
+ SubmitRequirementExpression.create("label:Code-Review=+2"),
SubmitRequirementExpressionResult.Status.FAIL,
ImmutableList.of(),
- ImmutableList.of("label:code-review=+2")))
+ ImmutableList.of("label:Code-Review=+2")))
.build()))
.build(),
newProtoBuilder()
@@ -726,7 +782,7 @@
SubmitRequirementProto.newBuilder()
.setName("Code-Review")
.setApplicabilityExpression("project:foo")
- .setSubmittabilityExpression("label:code-review=+2")
+ .setSubmittabilityExpression("label:Code-Review=+2")
.setAllowOverrideInChildProjects(false)
.build())
.setApplicabilityExpressionResult(
@@ -737,9 +793,9 @@
.build())
.setSubmittabilityExpressionResult(
SubmitRequirementExpressionResultProto.newBuilder()
- .setExpression("label:code-review=+2")
+ .setExpression("label:Code-Review=+2")
.setStatus("FAIL")
- .addFailingAtoms("label:code-review=+2")
+ .addFailingAtoms("label:Code-Review=+2")
.build())
.build())
.build());
@@ -752,9 +808,11 @@
.assigneeUpdates(
ImmutableList.of(
AssigneeStatusUpdate.create(
- new Timestamp(1212L), Account.id(1000), Optional.of(Account.id(2001))),
+ Instant.ofEpochMilli(1212L),
+ Account.id(1000),
+ Optional.of(Account.id(2001))),
AssigneeStatusUpdate.create(
- new Timestamp(3434L), Account.id(1000), Optional.empty())))
+ Instant.ofEpochMilli(3434L), Account.id(1000), Optional.empty())))
.build(),
ChangeNotesStateProto.newBuilder()
.setMetaId(SHA_BYTES)
@@ -799,7 +857,7 @@
ChangeMessage.create(
ChangeMessage.key(ID, "uuid1"),
Account.id(1000),
- new Timestamp(1212L),
+ Instant.ofEpochMilli(1212L),
PatchSet.id(ID, 1));
Entities.ChangeMessage m1Proto = ChangeMessageProtoConverter.INSTANCE.toProto(m1);
ByteString m1Bytes = Protos.toByteString(m1Proto);
@@ -809,7 +867,7 @@
ChangeMessage.create(
ChangeMessage.key(ID, "uuid2"),
Account.id(2000),
- new Timestamp(3434L),
+ Instant.ofEpochMilli(3434L),
PatchSet.id(ID, 2));
Entities.ChangeMessage m2Proto = ChangeMessageProtoConverter.INSTANCE.toProto(m2);
ByteString m2Bytes = Protos.toByteString(m2Proto);
@@ -833,7 +891,7 @@
new HumanComment(
new Comment.Key("uuid1", "file1", 1),
Account.id(1001),
- new Timestamp(1212L),
+ Instant.ofEpochMilli(1212L),
(short) 1,
"message 1",
"serverId",
@@ -845,7 +903,7 @@
new HumanComment(
new Comment.Key("uuid2", "file2", 2),
Account.id(1002),
- new Timestamp(3434L),
+ Instant.ofEpochMilli(3434L),
(short) 2,
"message 2",
"serverId",
@@ -921,7 +979,7 @@
"submitRequirementsResult",
new TypeLiteral<ImmutableList<SubmitRequirementResult>>() {}.getType())
.put("updateCount", int.class)
- .put("mergedOn", Timestamp.class)
+ .put("mergedOn", Instant.class)
.build());
}
@@ -931,8 +989,8 @@
.hasAutoValueMethods(
ImmutableMap.<String, Type>builder()
.put("changeKey", Change.Key.class)
- .put("createdOn", Timestamp.class)
- .put("lastUpdatedOn", Timestamp.class)
+ .put("createdOn", Instant.class)
+ .put("lastUpdatedOn", Instant.class)
.put("owner", Account.Id.class)
.put("branch", String.class)
.put("currentPatchSetId", PatchSet.Id.class)
@@ -958,7 +1016,7 @@
.put("id", PatchSet.Id.class)
.put("commitId", ObjectId.class)
.put("uploader", Account.Id.class)
- .put("createdOn", Timestamp.class)
+ .put("createdOn", Instant.class)
.put("groups", new TypeLiteral<ImmutableList<String>>() {}.getType())
.put("pushCertificate", new TypeLiteral<Optional<String>>() {}.getType())
.put("description", new TypeLiteral<Optional<String>>() {}.getType())
@@ -978,8 +1036,9 @@
.hasAutoValueMethods(
ImmutableMap.<String, Type>builder()
.put("key", PatchSetApproval.Key.class)
+ .put("uuid", new TypeLiteral<Optional<PatchSetApproval.UUID>>() {}.getType())
.put("value", short.class)
- .put("granted", Timestamp.class)
+ .put("granted", Instant.class)
.put("tag", new TypeLiteral<Optional<String>>() {}.getType())
.put("realAccountId", Account.Id.class)
.put("postSubmit", boolean.class)
@@ -995,7 +1054,7 @@
ImmutableMap.of(
"table",
new TypeLiteral<
- ImmutableTable<ReviewerStateInternal, Account.Id, Timestamp>>() {}.getType(),
+ ImmutableTable<ReviewerStateInternal, Account.Id, Instant>>() {}.getType(),
"accounts",
new TypeLiteral<ImmutableSet<Account.Id>>() {}.getType()));
}
@@ -1007,7 +1066,7 @@
ImmutableMap.of(
"table",
new TypeLiteral<
- ImmutableTable<ReviewerStateInternal, Address, Timestamp>>() {}.getType(),
+ ImmutableTable<ReviewerStateInternal, Address, Instant>>() {}.getType(),
"users",
new TypeLiteral<ImmutableSet<Address>>() {}.getType()));
}
@@ -1017,7 +1076,7 @@
assertThatSerializedClass(ReviewerStatusUpdate.class)
.hasAutoValueMethods(
ImmutableMap.of(
- "date", Timestamp.class,
+ "date", Instant.class,
"updatedBy", Account.Id.class,
"reviewer", Account.Id.class,
"state", ReviewerStateInternal.class));
@@ -1029,7 +1088,7 @@
.hasAutoValueMethods(
ImmutableMap.of(
"date",
- Timestamp.class,
+ Instant.class,
"updatedBy",
Account.Id.class,
"currentAssignee",
@@ -1067,7 +1126,7 @@
@Test
public void serializeMergedOn() throws Exception {
assertRoundTrip(
- newBuilder().mergedOn(new Timestamp(234567L)).build(),
+ newBuilder().mergedOn(Instant.ofEpochMilli(234567L)).build(),
ChangeNotesStateProto.newBuilder()
.setMetaId(SHA_BYTES)
.setChangeId(ID.get())
@@ -1086,7 +1145,7 @@
ImmutableMap.<String, Type>builder()
.put("key", ChangeMessage.Key.class)
.put("author", Account.Id.class)
- .put("writtenOn", Timestamp.class)
+ .put("writtenOn", Instant.class)
.put("message", String.class)
.put("patchset", PatchSet.Id.class)
.put("tag", String.class)
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index c524c94..09c8059 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.notedb;
import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.Truth8.assertThat;
@@ -61,7 +62,6 @@
import com.google.gerrit.server.validators.ValidationException;
import com.google.gerrit.testing.TestChanges;
import com.google.inject.Inject;
-import java.sql.Timestamp;
import java.time.Instant;
import java.util.LinkedHashSet;
import java.util.List;
@@ -131,7 +131,7 @@
1,
changeOwner,
null,
- TimeUtil.nowTs(),
+ TimeUtil.now(),
"Comment",
(short) 1,
commit,
@@ -193,7 +193,7 @@
1,
changeOwner,
null,
- TimeUtil.nowTs(),
+ TimeUtil.now(),
"Comment",
(short) 1,
commit,
@@ -244,12 +244,14 @@
assertThat(psas.get(0).label()).isEqualTo(LabelId.CODE_REVIEW);
assertThat(psas.get(0).value()).isEqualTo((short) -1);
assertThat(psas.get(0).granted()).isEqualTo(truncate(after(c, 2000)));
+ assertParsedUuid(psas.get(0));
assertThat(psas.get(1).patchSetId()).isEqualTo(c.currentPatchSetId());
assertThat(psas.get(1).accountId().get()).isEqualTo(1);
assertThat(psas.get(1).label()).isEqualTo(LabelId.VERIFIED);
assertThat(psas.get(1).value()).isEqualTo((short) 1);
assertThat(psas.get(1).granted()).isEqualTo(psas.get(0).granted());
+ assertParsedUuid(psas.get(1));
}
@Test
@@ -276,6 +278,7 @@
assertThat(psa1.label()).isEqualTo(LabelId.CODE_REVIEW);
assertThat(psa1.value()).isEqualTo((short) -1);
assertThat(psa1.granted()).isEqualTo(truncate(after(c, 2000)));
+ assertParsedUuid(psa1);
PatchSetApproval psa2 = Iterables.getOnlyElement(psas.get(ps2));
assertThat(psa2.patchSetId()).isEqualTo(ps2);
@@ -283,6 +286,7 @@
assertThat(psa2.label()).isEqualTo(LabelId.CODE_REVIEW);
assertThat(psa2.value()).isEqualTo((short) +1);
assertThat(psa2.granted()).isEqualTo(truncate(after(c, 4000)));
+ assertParsedUuid(psa2);
}
@Test
@@ -297,6 +301,7 @@
Iterables.getOnlyElement(notes.getApprovals().get(c.currentPatchSetId()));
assertThat(psa.label()).isEqualTo(LabelId.CODE_REVIEW);
assertThat(psa.value()).isEqualTo((short) -1);
+ assertParsedUuid(psa);
update = newUpdate(c, changeOwner);
update.putApproval(LabelId.CODE_REVIEW, (short) 1);
@@ -306,6 +311,7 @@
psa = Iterables.getOnlyElement(notes.getApprovals().get(c.currentPatchSetId()));
assertThat(psa.label()).isEqualTo(LabelId.CODE_REVIEW);
assertThat(psa.value()).isEqualTo((short) 1);
+ assertParsedUuid(psa);
}
@Test
@@ -329,12 +335,14 @@
assertThat(psas.get(0).label()).isEqualTo(LabelId.CODE_REVIEW);
assertThat(psas.get(0).value()).isEqualTo((short) -1);
assertThat(psas.get(0).granted()).isEqualTo(truncate(after(c, 2000)));
+ assertParsedUuid(psas.get(0));
assertThat(psas.get(1).patchSetId()).isEqualTo(c.currentPatchSetId());
assertThat(psas.get(1).accountId().get()).isEqualTo(2);
assertThat(psas.get(1).label()).isEqualTo(LabelId.CODE_REVIEW);
assertThat(psas.get(1).value()).isEqualTo((short) 1);
assertThat(psas.get(1).granted()).isEqualTo(truncate(after(c, 3000)));
+ assertParsedUuid(psas.get(1));
}
@Test
@@ -350,6 +358,7 @@
assertThat(psa.accountId().get()).isEqualTo(1);
assertThat(psa.label()).isEqualTo("Not-For-Long");
assertThat(psa.value()).isEqualTo((short) 1);
+ assertParsedUuid(psa);
update = newUpdate(c, changeOwner);
update.removeApproval("Not-For-Long");
@@ -368,6 +377,248 @@
}
@Test
+ public void approval_UUIDGenerated_forAllValues() throws Exception {
+ for (int value = -2; value <= 2; value++) {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.putApproval(LabelId.CODE_REVIEW, (short) value);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+ PatchSetApproval psa =
+ Iterables.getOnlyElement(notes.getApprovals().get(c.currentPatchSetId()));
+ assertThat(psa.accountId()).isEqualTo(changeOwner.getAccountId());
+ assertThat(psa.label()).isEqualTo(LabelId.CODE_REVIEW);
+ assertThat(psa.value()).isEqualTo((short) value);
+ assertParsedUuid(psa);
+ }
+ }
+
+ @Test
+ public void emptyApproval_uuidGenerated() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.putApproval(LabelId.CODE_REVIEW, (short) -1);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+ PatchSetApproval psa =
+ Iterables.getOnlyElement(notes.getApprovals().get(c.currentPatchSetId()));
+ assertThat(psa.accountId()).isEqualTo(changeOwner.getAccountId());
+ assertThat(psa.label()).isEqualTo(LabelId.CODE_REVIEW);
+ assertThat(psa.value()).isEqualTo((short) -1);
+ assertParsedUuid(psa);
+
+ update = newUpdate(c, changeOwner);
+ update.putApproval(LabelId.CODE_REVIEW, (short) 0);
+ update.commit();
+
+ notes = newNotes(c);
+ PatchSetApproval emptyPsa =
+ Iterables.getOnlyElement(notes.getApprovals().get(psa.patchSetId()));
+ assertThat(emptyPsa.key()).isEqualTo(psa.key());
+ assertThat(emptyPsa.value()).isEqualTo((short) 0);
+ assertThat(emptyPsa.label()).isEqualTo(psa.label());
+ assertParsedUuid(emptyPsa);
+ }
+
+ @Test
+ public void removedApproval_noUuid() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.putApproval(LabelId.CODE_REVIEW, (short) -1);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+ PatchSetApproval psa =
+ Iterables.getOnlyElement(notes.getApprovals().get(c.currentPatchSetId()));
+ assertThat(psa.accountId()).isEqualTo(changeOwner.getAccountId());
+ assertThat(psa.label()).isEqualTo(LabelId.CODE_REVIEW);
+ assertThat(psa.value()).isEqualTo((short) -1);
+ assertParsedUuid(psa);
+
+ update = newUpdate(c, changeOwner);
+ update.removeApproval(LabelId.CODE_REVIEW);
+ update.commit();
+
+ notes = newNotes(c);
+ PatchSetApproval removedPsa =
+ Iterables.getOnlyElement(notes.getApprovals().get(psa.patchSetId()));
+ assertThat(removedPsa.key()).isEqualTo(psa.key());
+ assertThat(removedPsa.value()).isEqualTo((short) 0);
+ assertThat(removedPsa.label()).isEqualTo(psa.label());
+ assertThat(removedPsa.uuid()).isEmpty();
+ }
+
+ @Test
+ public void reissuedApproval_samePatchSet_differentUUID() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.putApproval(LabelId.CODE_REVIEW, (short) -1);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+ assertThat(notes.getApprovals().keySet()).containsExactly(c.currentPatchSetId());
+ PatchSetApproval originalPsa =
+ Iterables.getOnlyElement(notes.getApprovals().get(c.currentPatchSetId()));
+
+ assertThat(originalPsa.patchSetId()).isEqualTo(c.currentPatchSetId());
+ assertThat(originalPsa.accountId()).isEqualTo(changeOwner.getAccountId());
+ assertThat(originalPsa.label()).isEqualTo(LabelId.CODE_REVIEW);
+ assertThat(originalPsa.value()).isEqualTo((short) -1);
+ assertParsedUuid(originalPsa);
+
+ // Remove approval from current patch set
+ update = newUpdate(c, changeOwner);
+ update.removeApproval(LabelId.CODE_REVIEW);
+ update.commit();
+
+ notes = newNotes(c);
+ assertThat(notes.getApprovals().keySet()).hasSize(1);
+ PatchSetApproval removedPsa =
+ Iterables.getOnlyElement(notes.getApprovals().get(c.currentPatchSetId()));
+ assertThat(removedPsa.key()).isEqualTo(originalPsa.key());
+ assertThat(removedPsa.value()).isEqualTo(0);
+ // Add approval with the same author, label, value to the current patch set
+ update = newUpdate(c, changeOwner);
+ update.putApproval(LabelId.CODE_REVIEW, (short) -1);
+ update.commit();
+
+ notes = newNotes(c);
+ assertThat(notes.getApprovals().keySet()).hasSize(1);
+ PatchSetApproval reAddedPsa =
+ Iterables.getOnlyElement(notes.getApprovals().get(c.currentPatchSetId()));
+
+ assertThat(reAddedPsa.key()).isEqualTo(originalPsa.key());
+ assertThat(reAddedPsa.value()).isEqualTo(originalPsa.value());
+ // The re-added approval has a different UUID
+ assertParsedUuid(reAddedPsa);
+ assertThat(reAddedPsa.uuid().get()).isNotEqualTo(originalPsa.uuid().get());
+ }
+
+ @Test
+ public void reissuedApproval_otherPatchSet_differentUUID() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.putApproval(LabelId.CODE_REVIEW, (short) -1);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+ assertThat(notes.getApprovals().keySet()).containsExactly(c.currentPatchSetId());
+ PatchSetApproval originalPsa =
+ Iterables.getOnlyElement(notes.getApprovals().get(c.currentPatchSetId()));
+
+ assertThat(originalPsa.patchSetId()).isEqualTo(c.currentPatchSetId());
+ assertThat(originalPsa.accountId().get()).isEqualTo(1);
+ assertThat(originalPsa.label()).isEqualTo(LabelId.CODE_REVIEW);
+ assertThat(originalPsa.value()).isEqualTo((short) -1);
+ assertParsedUuid(originalPsa);
+
+ // Create new PatchSet and re-issue vote
+ incrementPatchSet(c);
+ update = newUpdate(c, changeOwner);
+ update.putApproval(LabelId.CODE_REVIEW, (short) -1);
+ update.commit();
+
+ notes = newNotes(c);
+ assertThat(notes.getApprovals().keySet()).hasSize(2);
+ PatchSetApproval postUpdateOriginalPsa =
+ Iterables.getOnlyElement(notes.getApprovals().get(originalPsa.patchSetId()));
+ PatchSetApproval reAddedPsa =
+ Iterables.getOnlyElement(notes.getApprovals().get(c.currentPatchSetId()));
+
+ // Same patch set approval for the original patch set is returned after the vote was re-issued
+ // on the next patch set
+ assertThat(postUpdateOriginalPsa).isEqualTo(originalPsa);
+
+ assertThat(reAddedPsa.accountId()).isEqualTo(originalPsa.accountId());
+ assertThat(reAddedPsa.label()).isEqualTo(originalPsa.label());
+ assertThat(reAddedPsa.value()).isEqualTo(originalPsa.value());
+
+ // The re-added approval has a different UUID
+ assertParsedUuid(reAddedPsa);
+ assertThat(reAddedPsa.uuid().get()).isNotEqualTo(originalPsa.uuid().get());
+ }
+
+ @Test
+ public void approvalUUID_samePatchSet_differentUsers() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.putApproval(LabelId.CODE_REVIEW, (short) -1);
+ update.putApprovalFor(otherUserId, LabelId.CODE_REVIEW, (short) -1);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+ assertThat(notes.getApprovals().keySet()).containsExactly(c.currentPatchSetId());
+ List<PatchSetApproval> patchSetApprovals = notes.getApprovals().get(c.currentPatchSetId());
+ assertThat(patchSetApprovals).hasSize(2);
+ assertThat(
+ patchSetApprovals.stream()
+ .filter(psa -> psa.value() == (short) -1 && psa.label().equals(LabelId.CODE_REVIEW))
+ .count())
+ .isEqualTo(2);
+ // Count UUIDs to make sure they are unique
+ assertThat(patchSetApprovals.stream().map(psa -> psa.uuid().get()).distinct().count())
+ .isEqualTo(2);
+ assertThat(patchSetApprovals.stream().map(psa -> psa.accountId()).collect(toImmutableSet()))
+ .containsExactly(changeOwner.getAccountId(), otherUserId);
+ }
+
+ @Test
+ public void approvalUUID_samePatchSet_differentLabels() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.putApproval(LabelId.VERIFIED, (short) -1);
+ update.putApproval(LabelId.CODE_REVIEW, (short) -1);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+ assertThat(notes.getApprovals().keySet()).containsExactly(c.currentPatchSetId());
+ List<PatchSetApproval> patchSetApprovals = notes.getApprovals().get(c.currentPatchSetId());
+ assertThat(patchSetApprovals).hasSize(2);
+ assertThat(
+ patchSetApprovals.stream()
+ .filter(
+ psa ->
+ psa.value() == (short) -1
+ && psa.accountId().equals(changeOwner.getAccountId()))
+ .count())
+ .isEqualTo(2);
+
+ // Count UUIDs to make sure they are unique
+ assertThat(patchSetApprovals.stream().map(psa -> psa.uuid().get()).distinct().count())
+ .isEqualTo(2);
+ assertThat(patchSetApprovals.stream().map(psa -> psa.label()).collect(toImmutableSet()))
+ .containsExactly(LabelId.VERIFIED, LabelId.CODE_REVIEW);
+ }
+
+ @Test
+ public void approvalUUID_differentChanges() throws Exception {
+ Change c1 = newChange();
+ ChangeUpdate update1 = newUpdate(c1, changeOwner);
+ update1.putApproval(LabelId.CODE_REVIEW, (short) +2);
+ update1.commit();
+
+ Change c2 = newChange();
+ ChangeUpdate update = newUpdate(c2, changeOwner);
+ update.putApproval(LabelId.CODE_REVIEW, (short) +2);
+ update.commit();
+
+ ChangeNotes notes1 = newNotes(c1);
+ PatchSetApproval psa1 =
+ Iterables.getOnlyElement(notes1.getApprovals().get(c1.currentPatchSetId()));
+ ChangeNotes notes2 = newNotes(c2);
+ PatchSetApproval psa2 =
+ Iterables.getOnlyElement(notes2.getApprovals().get(c2.currentPatchSetId()));
+ assertThat(psa1.label()).isEqualTo(psa2.label());
+ assertThat(psa1.accountId()).isEqualTo(psa2.accountId());
+ assertThat(psa1.value()).isEqualTo(psa2.value());
+
+ // UUID is global: different across changes.
+ assertThat(psa1.uuid()).isNotEqualTo(psa2.uuid());
+ }
+
+ @Test
public void removeOtherUsersApprovals() throws Exception {
Change c = newChange();
ChangeUpdate update = newUpdate(c, otherUser);
@@ -380,21 +631,19 @@
assertThat(psa.accountId()).isEqualTo(otherUserId);
assertThat(psa.label()).isEqualTo("Not-For-Long");
assertThat(psa.value()).isEqualTo((short) 1);
+ assertParsedUuid(psa);
update = newUpdate(c, changeOwner);
update.removeApprovalFor(otherUserId, "Not-For-Long");
update.commit();
notes = newNotes(c);
- assertThat(notes.getApprovals())
- .containsExactlyEntriesIn(
- ImmutableListMultimap.of(
- psa.patchSetId(),
- PatchSetApproval.builder()
- .key(psa.key())
- .value(0)
- .granted(update.getWhen())
- .build()));
+ PatchSetApproval removedPsa =
+ Iterables.getOnlyElement(notes.getApprovals().get(psa.patchSetId()));
+ assertThat(removedPsa.key()).isEqualTo(psa.key());
+ assertThat(removedPsa.value()).isEqualTo((short) 0);
+ assertThat(removedPsa.label()).isEqualTo(psa.label());
+ assertThat(removedPsa.uuid()).isEmpty();
// Add back approval on same label.
update = newUpdate(c, otherUser);
@@ -406,6 +655,7 @@
assertThat(psa.accountId()).isEqualTo(otherUserId);
assertThat(psa.label()).isEqualTo("Not-For-Long");
assertThat(psa.value()).isEqualTo((short) 2);
+ assertParsedUuid(psa);
}
@Test
@@ -426,10 +676,13 @@
assertThat(approvals.get(0).accountId()).isEqualTo(changeOwner.getAccountId());
assertThat(approvals.get(0).label()).isEqualTo(LabelId.CODE_REVIEW);
assertThat(approvals.get(0).value()).isEqualTo((short) 1);
+ assertParsedUuid(approvals.get(0));
assertThat(approvals.get(1).accountId()).isEqualTo(otherUser.getAccountId());
assertThat(approvals.get(1).label()).isEqualTo(LabelId.CODE_REVIEW);
assertThat(approvals.get(1).value()).isEqualTo((short) -1);
+ assertThat(approvals.get(1).uuid()).isPresent();
+ assertParsedUuid(approvals.get(1));
}
@Test
@@ -462,9 +715,12 @@
assertThat(approvals.get(0).label()).isEqualTo(LabelId.VERIFIED);
assertThat(approvals.get(0).value()).isEqualTo((short) 1);
assertThat(approvals.get(0).postSubmit()).isFalse();
+ assertParsedUuid(approvals.get(1));
+
assertThat(approvals.get(1).label()).isEqualTo(LabelId.CODE_REVIEW);
assertThat(approvals.get(1).value()).isEqualTo((short) 2);
assertThat(approvals.get(1).postSubmit()).isTrue();
+ assertParsedUuid(approvals.get(1));
}
@Test
@@ -503,14 +759,154 @@
assertThat(approvals.get(0).label()).isEqualTo(LabelId.VERIFIED);
assertThat(approvals.get(0).value()).isEqualTo(1);
assertThat(approvals.get(0).postSubmit()).isFalse();
+ assertParsedUuid(approvals.get(0));
+
assertThat(approvals.get(1).accountId()).isEqualTo(ownerId);
assertThat(approvals.get(1).label()).isEqualTo(LabelId.CODE_REVIEW);
assertThat(approvals.get(1).value()).isEqualTo(2);
assertThat(approvals.get(1).postSubmit()).isFalse(); // During submit.
+ assertParsedUuid(approvals.get(1));
+
assertThat(approvals.get(2).accountId()).isEqualTo(otherId);
assertThat(approvals.get(2).label()).isEqualTo("Other-Label");
assertThat(approvals.get(2).value()).isEqualTo(2);
assertThat(approvals.get(2).postSubmit()).isTrue();
+ assertParsedUuid(approvals.get(2));
+ }
+
+ @Test
+ public void copiedApprovals_keepsUUID() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.putApproval(LabelId.CODE_REVIEW, (short) 2);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+ PatchSetApproval originalPsa =
+ Iterables.getOnlyElement(notes.getApprovals().get(c.currentPatchSetId()));
+ assertThat(originalPsa.accountId()).isEqualTo(changeOwner.getAccountId());
+ assertThat(originalPsa.label()).isEqualTo(LabelId.CODE_REVIEW);
+ assertThat(originalPsa.value()).isEqualTo(2);
+ assertThat(originalPsa.tag()).isEmpty();
+ assertThat(originalPsa.realAccountId()).isEqualTo(changeOwner.getAccountId());
+ assertParsedUuid(originalPsa);
+
+ // Copied approvals are persisted at the patch set upload, add new patch set
+ incrementPatchSet(c);
+
+ addCopiedApproval(c, changeOwner, originalPsa);
+
+ notes = newNotes(c);
+ assertThat(notes.getApprovalsWithCopied().keySet()).hasSize(2);
+ PatchSetApproval copiedApproval =
+ Iterables.getOnlyElement(
+ notes.getApprovalsWithCopied().get(c.currentPatchSetId()).stream()
+ .filter(a -> a.copied())
+ .collect(toImmutableList()));
+ PatchSetApproval nonCopiedApproval =
+ Iterables.getOnlyElement(
+ notes.getApprovalsWithCopied().get(originalPsa.patchSetId()).stream()
+ .filter(a -> !a.copied())
+ .collect(toImmutableList()));
+
+ // Still same original PSA is returned
+ assertThat(nonCopiedApproval).isEqualTo(originalPsa);
+
+ // The copied approval matches the original approval, including UUID
+ assertCopiedApproval(originalPsa, copiedApproval);
+ }
+
+ @Test
+ public void copiedApprovals_withRealUserAndTag_keepsUUID() throws Exception {
+ ImmutableList<String> strangeTags =
+ ImmutableList.of(", ", ":\"", ",", "!@#$%^\0&*):\" \n: \r\"#$@,. :");
+ for (String strangeTag : strangeTags) {
+ Change c = newChange();
+ CurrentUser otherUserAsOwner = userFactory.runAs(null, changeOwner.getAccountId(), otherUser);
+ ChangeUpdate update = newUpdate(c, otherUserAsOwner);
+ update.putApproval(LabelId.CODE_REVIEW, (short) 2);
+ update.setTag(strangeTag);
+ update.commit();
+ ChangeNotes notes = newNotes(c);
+ PatchSetApproval originalPsa =
+ Iterables.getOnlyElement(notes.getApprovals().get(c.currentPatchSetId()));
+ assertThat(originalPsa.accountId()).isEqualTo(changeOwner.getAccountId());
+ assertThat(originalPsa.label()).isEqualTo(LabelId.CODE_REVIEW);
+ assertThat(originalPsa.value()).isEqualTo(2);
+ assertThat(originalPsa.tag()).hasValue(NoteDbUtil.sanitizeFooter(strangeTag));
+ assertThat(originalPsa.realAccountId()).isEqualTo(otherUserId);
+ assertParsedUuid(originalPsa);
+
+ // Copied approvals are persisted at the patch set upload, add new patch set
+ incrementPatchSet(c);
+
+ addCopiedApproval(c, changeOwner, originalPsa);
+
+ notes = newNotes(c);
+ assertThat(notes.getApprovalsWithCopied().keySet()).hasSize(2);
+ PatchSetApproval copiedApproval =
+ Iterables.getOnlyElement(
+ notes.getApprovalsWithCopied().get(c.currentPatchSetId()).stream()
+ .filter(a -> a.copied())
+ .collect(toImmutableList()));
+ PatchSetApproval nonCopiedApproval =
+ Iterables.getOnlyElement(
+ notes.getApprovalsWithCopied().get(originalPsa.patchSetId()).stream()
+ .filter(a -> !a.copied())
+ .collect(toImmutableList()));
+
+ // Still same original PSA is returned
+ assertThat(nonCopiedApproval).isEqualTo(originalPsa);
+
+ // The copied approval matches the original approval, including UUID
+ assertCopiedApproval(originalPsa, copiedApproval);
+ }
+ }
+
+ @Test
+ public void copiedApprovals_withTag_keepsUUID() throws Exception {
+ ImmutableList<String> strangeTags =
+ ImmutableList.of(", ", ":\"", ",", "!@#$%^\0&*):\" \n: \r\"#$@,. :");
+ for (String strangeTag : strangeTags) {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.putApproval(LabelId.CODE_REVIEW, (short) 2);
+ update.setTag(strangeTag);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+ PatchSetApproval originalPsa =
+ Iterables.getOnlyElement(notes.getApprovals().get(c.currentPatchSetId()));
+ assertThat(originalPsa.accountId()).isEqualTo(changeOwner.getAccountId());
+ assertThat(originalPsa.label()).isEqualTo(LabelId.CODE_REVIEW);
+ assertThat(originalPsa.value()).isEqualTo(2);
+ assertThat(originalPsa.tag()).hasValue(NoteDbUtil.sanitizeFooter(strangeTag));
+ assertThat(originalPsa.realAccountId()).isEqualTo(changeOwner.getAccountId());
+ assertParsedUuid(originalPsa);
+ // Copied approvals are persisted at the patch set upload, add new patch set
+ incrementPatchSet(c);
+
+ addCopiedApproval(c, changeOwner, originalPsa);
+
+ notes = newNotes(c);
+ assertThat(notes.getApprovalsWithCopied().keySet()).hasSize(2);
+ PatchSetApproval copiedApproval =
+ Iterables.getOnlyElement(
+ notes.getApprovalsWithCopied().get(c.currentPatchSetId()).stream()
+ .filter(a -> a.copied())
+ .collect(toImmutableList()));
+ PatchSetApproval nonCopiedApproval =
+ Iterables.getOnlyElement(
+ notes.getApprovalsWithCopied().get(originalPsa.patchSetId()).stream()
+ .filter(a -> !a.copied())
+ .collect(toImmutableList()));
+
+ // Still same original PSA is returned
+ assertThat(nonCopiedApproval).isEqualTo(originalPsa);
+
+ // The copied approval matches the original approval, including UUID
+ assertCopiedApproval(originalPsa, copiedApproval);
+ }
}
@Test
@@ -526,7 +922,7 @@
LabelId.create(LabelId.CODE_REVIEW)))
.value(1)
.copied(true)
- .granted(TimeUtil.nowTs())
+ .granted(TimeUtil.now())
.tag("tag")
.realAccountId(otherUserId)
.build());
@@ -578,7 +974,7 @@
LabelId.create(LabelId.CODE_REVIEW)))
.value(1)
.copied(true)
- .granted(TimeUtil.nowTs())
+ .granted(TimeUtil.now())
.build());
update.commit();
@@ -611,7 +1007,7 @@
LabelId.create(LabelId.CODE_REVIEW)))
.value(1)
.copied(true)
- .granted(TimeUtil.nowTs())
+ .granted(TimeUtil.now())
.tag(strangeTag)
.build());
update.commit();
@@ -640,7 +1036,7 @@
LabelId.create(LabelId.CODE_REVIEW)))
.value(1)
.copied(true)
- .granted(TimeUtil.nowTs())
+ .granted(TimeUtil.now())
.build());
update.putCopiedApproval(
PatchSetApproval.builder()
@@ -651,7 +1047,7 @@
LabelId.create(LabelId.VERIFIED)))
.value(1)
.copied(true)
- .granted(TimeUtil.nowTs())
+ .granted(TimeUtil.now())
.build());
update.commit();
@@ -676,7 +1072,7 @@
LabelId.create(LabelId.CODE_REVIEW)))
.value(2)
.copied(true)
- .granted(TimeUtil.nowTs())
+ .granted(TimeUtil.now())
.build());
update.commit();
@@ -700,11 +1096,11 @@
update.commit();
ChangeNotes notes = newNotes(c);
- Timestamp ts = new Timestamp(update.getWhen().getTime());
+ Instant ts = update.getWhen();
assertThat(notes.getReviewers())
.isEqualTo(
ReviewerSet.fromTable(
- ImmutableTable.<ReviewerStateInternal, Account.Id, Timestamp>builder()
+ ImmutableTable.<ReviewerStateInternal, Account.Id, Instant>builder()
.put(REVIEWER, Account.id(1), ts)
.put(REVIEWER, Account.id(2), ts)
.build()));
@@ -719,11 +1115,11 @@
update.commit();
ChangeNotes notes = newNotes(c);
- Timestamp ts = new Timestamp(update.getWhen().getTime());
+ Instant ts = update.getWhen();
assertThat(notes.getReviewers())
.isEqualTo(
ReviewerSet.fromTable(
- ImmutableTable.<ReviewerStateInternal, Account.Id, Timestamp>builder()
+ ImmutableTable.<ReviewerStateInternal, Account.Id, Instant>builder()
.put(REVIEWER, Account.id(1), ts)
.put(CC, Account.id(2), ts)
.build()));
@@ -737,7 +1133,7 @@
update.commit();
ChangeNotes notes = newNotes(c);
- Timestamp ts = new Timestamp(update.getWhen().getTime());
+ Instant ts = update.getWhen();
assertThat(notes.getReviewers())
.isEqualTo(ReviewerSet.fromTable(ImmutableTable.of(REVIEWER, Account.id(2), ts)));
@@ -746,7 +1142,7 @@
update.commit();
notes = newNotes(c);
- ts = new Timestamp(update.getWhen().getTime());
+ ts = update.getWhen();
assertThat(notes.getReviewers())
.isEqualTo(ReviewerSet.fromTable(ImmutableTable.of(CC, Account.id(2), ts)));
}
@@ -881,7 +1277,7 @@
update.commit();
ChangeNotes notes = newNotes(c);
assertThat(notes.getMergedOn()).isPresent();
- Timestamp mergedOn = notes.getMergedOn().get();
+ Instant mergedOn = notes.getMergedOn().get();
assertThat(mergedOn).isEqualTo(notes.getChange().getLastUpdatedOn());
// Next update does not change mergedOn date.
@@ -907,7 +1303,7 @@
update.commit();
ChangeNotes notes = newNotes(c);
assertThat(notes.getMergedOn()).isPresent();
- Timestamp mergedOn = notes.getMergedOn().get();
+ Instant mergedOn = notes.getMergedOn().get();
assertThat(mergedOn).isEqualTo(notes.getChange().getLastUpdatedOn());
incrementPatchSet(c);
@@ -1301,7 +1697,7 @@
public void createdOnChangeNotes() throws Exception {
Change c = newChange();
- Timestamp createdOn = newNotes(c).getChange().getCreatedOn();
+ Instant createdOn = newNotes(c).getChange().getCreatedOn();
assertThat(createdOn).isNotNull();
// An update doesn't affect the createdOn timestamp.
@@ -1316,54 +1712,54 @@
Change c = newChange();
ChangeNotes notes = newNotes(c);
- Timestamp ts1 = notes.getChange().getLastUpdatedOn();
+ Instant ts1 = notes.getChange().getLastUpdatedOn();
assertThat(ts1).isEqualTo(notes.getChange().getCreatedOn());
// Various kinds of updates that update the timestamp.
ChangeUpdate update = newUpdate(c, changeOwner);
update.setTopic("topic"); // Change something to get a new commit.
update.commit();
- Timestamp ts2 = newNotes(c).getChange().getLastUpdatedOn();
+ Instant ts2 = newNotes(c).getChange().getLastUpdatedOn();
assertThat(ts2).isGreaterThan(ts1);
update = newUpdate(c, changeOwner);
update.setChangeMessage("Some message");
update.commit();
- Timestamp ts3 = newNotes(c).getChange().getLastUpdatedOn();
+ Instant ts3 = newNotes(c).getChange().getLastUpdatedOn();
assertThat(ts3).isGreaterThan(ts2);
update = newUpdate(c, changeOwner);
update.setHashtags(ImmutableSet.of("foo"));
update.commit();
- Timestamp ts4 = newNotes(c).getChange().getLastUpdatedOn();
+ Instant ts4 = newNotes(c).getChange().getLastUpdatedOn();
assertThat(ts4).isGreaterThan(ts3);
incrementPatchSet(c);
- Timestamp ts5 = newNotes(c).getChange().getLastUpdatedOn();
+ Instant ts5 = newNotes(c).getChange().getLastUpdatedOn();
assertThat(ts5).isGreaterThan(ts4);
update = newUpdate(c, changeOwner);
update.putApproval(LabelId.CODE_REVIEW, (short) 1);
update.commit();
- Timestamp ts6 = newNotes(c).getChange().getLastUpdatedOn();
+ Instant ts6 = newNotes(c).getChange().getLastUpdatedOn();
assertThat(ts6).isGreaterThan(ts5);
update = newUpdate(c, changeOwner);
update.setStatus(Change.Status.ABANDONED);
update.commit();
- Timestamp ts7 = newNotes(c).getChange().getLastUpdatedOn();
+ Instant ts7 = newNotes(c).getChange().getLastUpdatedOn();
assertThat(ts7).isGreaterThan(ts6);
update = newUpdate(c, changeOwner);
update.putReviewer(otherUser.getAccountId(), ReviewerStateInternal.REVIEWER);
update.commit();
- Timestamp ts8 = newNotes(c).getChange().getLastUpdatedOn();
+ Instant ts8 = newNotes(c).getChange().getLastUpdatedOn();
assertThat(ts8).isGreaterThan(ts7);
update = newUpdate(c, changeOwner);
update.setGroups(ImmutableList.of("a", "b"));
update.commit();
- Timestamp ts9 = newNotes(c).getChange().getLastUpdatedOn();
+ Instant ts9 = newNotes(c).getChange().getLastUpdatedOn();
assertThat(ts9).isGreaterThan(ts8);
// Finish off by merging the change.
@@ -1377,7 +1773,7 @@
submitLabel(LabelId.VERIFIED, "OK", changeOwner.getAccountId()),
submitLabel("Alternative-Code-Review", "NEED", null))));
update.commit();
- Timestamp ts10 = newNotes(c).getChange().getLastUpdatedOn();
+ Instant ts10 = newNotes(c).getChange().getLastUpdatedOn();
assertThat(ts10).isGreaterThan(ts9);
}
@@ -1490,7 +1886,7 @@
1,
changeOwner,
null,
- TimeUtil.nowTs(),
+ TimeUtil.now(),
"Comment",
(short) 1,
commit,
@@ -1579,7 +1975,7 @@
// comment on ps2
update = newUpdate(c, changeOwner);
update.setPatchSetId(psId2);
- Timestamp ts = TimeUtil.nowTs();
+ Instant ts = TimeUtil.now();
update.putComment(
HumanComment.Status.PUBLISHED,
newComment(
@@ -1647,7 +2043,7 @@
String uuid1 = "uuid1";
String message1 = "comment 1";
CommentRange range1 = new CommentRange(1, 1, 2, 1);
- Timestamp time1 = TimeUtil.nowTs();
+ Instant time1 = TimeUtil.now();
PatchSet.Id psId = c.currentPatchSetId();
RevCommit tipCommit;
try (NoteDbUpdateManager updateManager = updateManagerFactory.create(project)) {
@@ -1896,7 +2292,7 @@
0,
otherUser,
null,
- TimeUtil.nowTs(),
+ TimeUtil.now(),
"message",
(short) 1,
commitId,
@@ -1926,7 +2322,7 @@
range.getEndLine(),
otherUser,
null,
- TimeUtil.nowTs(),
+ TimeUtil.now(),
"message",
(short) 1,
commitId,
@@ -1956,7 +2352,7 @@
range.getEndLine(),
otherUser,
null,
- TimeUtil.nowTs(),
+ TimeUtil.now(),
"message",
(short) 1,
commitId,
@@ -1986,7 +2382,7 @@
range.getEndLine(),
otherUser,
null,
- TimeUtil.nowTs(),
+ TimeUtil.now(),
"message",
(short) 1,
commitId,
@@ -2013,7 +2409,7 @@
String message3 = "comment 3";
CommentRange range1 = new CommentRange(1, 1, 2, 1);
CommentRange range2 = new CommentRange(2, 1, 3, 1);
- Timestamp time = TimeUtil.nowTs();
+ Instant time = TimeUtil.now();
ObjectId commitId = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
HumanComment comment1 =
@@ -2083,7 +2479,7 @@
String uuid = "uuid";
String message = "comment";
CommentRange range = new CommentRange(1, 1, 2, 1);
- Timestamp time = TimeUtil.nowTs();
+ Instant time = TimeUtil.now();
PatchSet.Id psId = c.currentPatchSetId();
ObjectId commitId = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
@@ -2113,7 +2509,7 @@
@Test
public void patchLineCommentNotesFormatWeirdUser() throws Exception {
- Account.Builder account = Account.builder(Account.id(3), TimeUtil.nowTs());
+ Account.Builder account = Account.builder(Account.id(3), TimeUtil.now());
account.setFullName("Weird\n\u0002<User>\n");
account.setPreferredEmail(" we\r\nird@ex>ample<.com");
accountCache.put(account.build());
@@ -2123,7 +2519,7 @@
ChangeUpdate update = newUpdate(c, user);
String uuid = "uuid";
CommentRange range = new CommentRange(1, 1, 2, 1);
- Timestamp time = TimeUtil.nowTs();
+ Instant time = TimeUtil.now();
PatchSet.Id psId = c.currentPatchSetId();
HumanComment comment =
@@ -2161,7 +2557,7 @@
String messageForBase = "comment for base";
String messageForPS = "comment for ps";
CommentRange range = new CommentRange(1, 1, 2, 1);
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
PatchSet.Id psId = c.currentPatchSetId();
HumanComment commentForBase =
@@ -2220,8 +2616,8 @@
short side = (short) 1;
ChangeUpdate update = newUpdate(c, otherUser);
- Timestamp timeForComment1 = TimeUtil.nowTs();
- Timestamp timeForComment2 = TimeUtil.nowTs();
+ Instant timeForComment1 = TimeUtil.now();
+ Instant timeForComment2 = TimeUtil.now();
HumanComment comment1 =
newComment(
psId,
@@ -2279,7 +2675,7 @@
short side = (short) 1;
ChangeUpdate update = newUpdate(c, otherUser);
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
HumanComment comment1 =
newComment(
psId,
@@ -2337,7 +2733,7 @@
short side = (short) 1;
ChangeUpdate update = newUpdate(c, otherUser);
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
HumanComment comment1 =
newComment(
ps1,
@@ -2360,7 +2756,7 @@
PatchSet.Id ps2 = c.currentPatchSetId();
update = newUpdate(c, otherUser);
- now = TimeUtil.nowTs();
+ now = TimeUtil.now();
HumanComment comment2 =
newComment(
ps2,
@@ -2397,7 +2793,7 @@
short side = (short) 1;
ChangeUpdate update = newUpdate(c, otherUser);
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
HumanComment comment1 =
newComment(
ps1,
@@ -2442,7 +2838,7 @@
CommentRange range2 = new CommentRange(2, 2, 3, 3);
String filename = "filename1";
short side = (short) 1;
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
PatchSet.Id psId = c.currentPatchSetId();
// Write two drafts on the same side of one patch set.
@@ -2512,7 +2908,7 @@
CommentRange range1 = new CommentRange(1, 1, 2, 2);
CommentRange range2 = new CommentRange(2, 2, 3, 3);
String filename = "filename1";
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
PatchSet.Id psId = c.currentPatchSetId();
// Write two drafts, one on each side of the patchset.
@@ -2587,7 +2983,7 @@
short side = (short) 1;
ChangeUpdate update = newUpdate(c, otherUser);
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
HumanComment comment =
newComment(
psId,
@@ -2632,7 +3028,7 @@
short side = (short) 1;
ChangeUpdate update = newUpdate(c, otherUser);
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
HumanComment comment1 =
newComment(
ps1,
@@ -2655,7 +3051,7 @@
PatchSet.Id ps2 = c.currentPatchSetId();
update = newUpdate(c, otherUser);
- now = TimeUtil.nowTs();
+ now = TimeUtil.now();
HumanComment comment2 =
newComment(
ps2,
@@ -2700,7 +3096,7 @@
short side = (short) 1;
ChangeUpdate update = newUpdate(c, otherUser);
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
HumanComment comment =
newComment(
ps1,
@@ -2733,7 +3129,7 @@
short side = (short) 1;
ChangeUpdate update = newUpdate(c, otherUser);
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
HumanComment draft =
newComment(
ps1,
@@ -2783,7 +3179,7 @@
String uuid = "uuid";
ObjectId commitId = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
String messageForBase = "comment for base";
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
PatchSet.Id psId = c.currentPatchSetId();
HumanComment comment =
@@ -2815,7 +3211,7 @@
String uuid = "uuid";
ObjectId commitId = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
String messageForBase = "comment for base";
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
PatchSet.Id psId = c.currentPatchSetId();
HumanComment comment =
@@ -2856,7 +3252,7 @@
ChangeUpdate update = newUpdate(c, otherUser);
update.setPatchSetId(ps2);
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
HumanComment comment1 =
newComment(
ps1,
@@ -2914,7 +3310,7 @@
ChangeUpdate update = newUpdate(c, otherUser);
update.setPatchSetId(ps1);
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
HumanComment comment1 =
newComment(
ps1,
@@ -2988,7 +3384,7 @@
short side = (short) 1;
ChangeUpdate update = newUpdate(c, otherUser);
- Timestamp now = TimeUtil.nowTs();
+ Instant now = TimeUtil.now();
HumanComment comment1 =
newComment(
ps1,
@@ -3075,7 +3471,7 @@
range.getEndLine(),
otherUser,
null,
- new Timestamp(update1.getWhen().getTime()),
+ update1.getWhen(),
"comment 1",
(short) 1,
commitId,
@@ -3092,7 +3488,7 @@
range.getEndLine(),
otherUser,
null,
- new Timestamp(update2.getWhen().getTime()),
+ update2.getWhen(),
"comment 2",
(short) 1,
commitId,
@@ -3149,7 +3545,7 @@
range.getEndLine(),
changeOwner,
null,
- new Timestamp(update.getWhen().getTime()),
+ update.getWhen(),
"comment",
(short) 1,
ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234"),
@@ -3585,11 +3981,45 @@
}
private AttentionSetUpdate addTimestamp(AttentionSetUpdate attentionSetUpdate, Change c) {
- Timestamp timestamp = newNotes(c).getChange().getLastUpdatedOn();
+ Instant timestamp = newNotes(c).getChange().getLastUpdatedOn();
return AttentionSetUpdate.createFromRead(
- timestamp.toInstant(),
+ timestamp,
attentionSetUpdate.account(),
attentionSetUpdate.operation(),
attentionSetUpdate.reason());
}
+
+ /**
+ * Assert UUID was parsed as generated by {@link
+ * com.google.gerrit.server.approval.testing.TestPatchSetApprovalUuidGenerator}.
+ */
+ private void assertParsedUuid(PatchSetApproval patchSetApproval) {
+ assertThat(patchSetApproval.uuid().get().get()).matches("^[0-9a-z_]+$");
+ }
+
+ private void assertCopiedApproval(PatchSetApproval originalPsa, PatchSetApproval copiedPsa) {
+ assertThat(copiedPsa.label()).isEqualTo(originalPsa.label());
+ assertThat(copiedPsa.value()).isEqualTo(originalPsa.value());
+ assertThat(copiedPsa.tag()).isEqualTo(originalPsa.tag());
+ assertThat(copiedPsa.accountId()).isEqualTo(originalPsa.accountId());
+ assertThat(copiedPsa.realAccountId()).isEqualTo(originalPsa.realAccountId());
+ assertThat(copiedPsa.uuid()).isEqualTo(originalPsa.uuid());
+ assertThat(copiedPsa.copied()).isTrue();
+ }
+
+ private void addCopiedApproval(Change c, CurrentUser user, PatchSetApproval originalPsa)
+ throws Exception {
+ ChangeUpdate update = newUpdate(c, user);
+ update.putCopiedApproval(
+ PatchSetApproval.builder()
+ .key(originalPsa.key())
+ .value(originalPsa.value())
+ .copied(true)
+ .granted(TimeUtil.now())
+ .tag(originalPsa.tag())
+ .uuid(originalPsa.uuid())
+ .realAccountId(originalPsa.realAccountId())
+ .build());
+ update.commit();
+ }
}
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeUpdateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeUpdateTest.java
index fa05adc..cf1b5ae 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeUpdateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeUpdateTest.java
@@ -81,7 +81,7 @@
1,
changeOwner,
null,
- TimeUtil.nowTs(),
+ TimeUtil.now(),
"Comment",
(short) 1,
commit,
diff --git a/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java b/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
index 2c1348c..2191f00 100644
--- a/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
@@ -34,7 +34,7 @@
/** Arbitrary time outside of a DST transition, as an ISO instant. */
private static final String NON_DST_STR = "2017-02-07T10:20:30.123Z";
- /** Arbitrary time outside of a DST transition, as a reasonable Java 8 representation. */
+ /** Arbitrary time outside of a DST transition, as a reasonable Java 11 representation. */
private static final ZonedDateTime NON_DST = ZonedDateTime.parse(NON_DST_STR);
/** {@link #NON_DST_STR} truncated to seconds. */
@@ -155,7 +155,7 @@
new HumanComment(
new Comment.Key("uuid", "filename", 1),
Account.id(100),
- NON_DST_TS,
+ NON_DST_TS.toInstant(),
(short) 0,
"message",
"serverId",
diff --git a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index 68a1d9d..5e2e1f2 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -27,7 +27,6 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.testing.TestChanges;
-import java.util.Date;
import java.util.TimeZone;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
@@ -36,6 +35,9 @@
import org.junit.Test;
public class CommitMessageOutputTest extends AbstractChangeNotesTest {
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Test
public void approvalsCommitFormatSimple() throws Exception {
Change c = TestChanges.newChange(project, changeOwner.getAccountId(), 1);
@@ -61,20 +63,20 @@
+ "\n"
+ "Reviewer: Gerrit User 1 <1@gerrit>\n"
+ "CC: Gerrit User 2 <2@gerrit>\n"
- + "Label: Code-Review=-1\n"
- + "Label: Verified=+1\n",
+ + "Label: Code-Review=-1, 1_1_1_code_review__1_1\n"
+ + "Label: Verified=+1, 1_1_1_verified_1_2\n",
commit);
PersonIdent author = commit.getAuthorIdent();
assertThat(author.getName()).isEqualTo("Gerrit User 1");
assertThat(author.getEmailAddress()).isEqualTo("1@gerrit");
- assertThat(author.getWhen()).isEqualTo(new Date(c.getCreatedOn().getTime() + 1000));
+ assertThat(author.getWhen().getTime()).isEqualTo(c.getCreatedOn().toEpochMilli() + 1000);
assertThat(author.getTimeZone()).isEqualTo(TimeZone.getTimeZone("GMT-7:00"));
PersonIdent committer = commit.getCommitterIdent();
assertThat(committer.getName()).isEqualTo("Gerrit Server");
assertThat(committer.getEmailAddress()).isEqualTo("noreply@gerrit.com");
- assertThat(committer.getWhen()).isEqualTo(author.getWhen());
+ assertThat(committer.getWhen().getTime()).isEqualTo(author.getWhen().getTime());
assertThat(committer.getTimeZone()).isEqualTo(author.getTimeZone());
}
@@ -143,6 +145,9 @@
}
@Test
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
public void submitCommitFormat() throws Exception {
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
@@ -184,20 +189,20 @@
PersonIdent author = commit.getAuthorIdent();
assertThat(author.getName()).isEqualTo("Gerrit User 1");
assertThat(author.getEmailAddress()).isEqualTo("1@gerrit");
- assertThat(author.getWhen()).isEqualTo(new Date(c.getCreatedOn().getTime() + 2000));
+ assertThat(author.getWhen().getTime()).isEqualTo(c.getCreatedOn().toEpochMilli() + 2000);
assertThat(author.getTimeZone()).isEqualTo(TimeZone.getTimeZone("GMT-7:00"));
PersonIdent committer = commit.getCommitterIdent();
assertThat(committer.getName()).isEqualTo("Gerrit Server");
assertThat(committer.getEmailAddress()).isEqualTo("noreply@gerrit.com");
- assertThat(committer.getWhen()).isEqualTo(author.getWhen());
+ assertThat(committer.getWhen().getTime()).isEqualTo(author.getWhen().getTime());
assertThat(committer.getTimeZone()).isEqualTo(author.getTimeZone());
}
@Test
public void anonymousUser() throws Exception {
Account anon =
- Account.builder(Account.id(3), TimeUtil.nowTs())
+ Account.builder(Account.id(3), TimeUtil.now())
.setMetaId("1234567812345678123456781234567812345678")
.build();
accountCache.put(anon);
diff --git a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
index 98721fd..3b18183 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
@@ -47,10 +47,13 @@
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gson.Gson;
import com.google.inject.Inject;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.stream.IntStream;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.ObjectId;
@@ -261,6 +264,7 @@
Ref metaRefBeforeRewrite = repo.exactRef(refName);
expectedSkippedRefsToOldMetaBuilder.put(refName, metaRefBeforeRewrite.getObjectId());
}
+ Set<String> invalidRefs = new HashSet<>();
for (int i = 0; i < numberOfInvalidChanges; i++) {
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
@@ -270,12 +274,20 @@
updateWithSubject.setSubjectForCommit("Update with subject");
updateWithSubject.commit();
String refName = RefNames.changeMetaRef(c.getId());
- Ref metaRefBeforeRewrite = repo.exactRef(refName);
- if (i < maxRefsToUpdate) {
- expectedFixedRefsToOldMetaBuilder.put(refName, metaRefBeforeRewrite.getObjectId());
- } else {
- expectedSkippedRefsToOldMetaBuilder.put(refName, metaRefBeforeRewrite.getObjectId());
+ invalidRefs.add(refName);
+ }
+ int i = 0;
+ for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_CHANGES)) {
+ Ref metaRefBeforeRewrite = repo.exactRef(ref.getName());
+ if (!invalidRefs.contains(ref.getName())) {
+ continue;
}
+ if (i < maxRefsToUpdate) {
+ expectedFixedRefsToOldMetaBuilder.put(ref.getName(), metaRefBeforeRewrite.getObjectId());
+ } else {
+ expectedSkippedRefsToOldMetaBuilder.put(ref.getName(), metaRefBeforeRewrite.getObjectId());
+ }
+ i++;
}
ImmutableMap<String, ObjectId> expectedFixedRefsToOldMeta =
expectedFixedRefsToOldMetaBuilder.build();
@@ -309,15 +321,18 @@
assertThat(secondRunResult.fixedRefDiff.keySet().size()).isEqualTo(expectedSecondRunResult);
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Test
public void fixAuthorIdent() throws Exception {
Change c = newChange();
- Timestamp when = TimeUtil.nowTs();
+ Instant when = TimeUtil.now();
PersonIdent invalidAuthorIdent =
new PersonIdent(
changeOwner.getName(),
changeNoteUtil.getAccountIdAsEmailAddress(changeOwner.getAccountId()),
- when,
+ Date.from(when),
serverIdent.getTimeZone());
RevCommit invalidUpdateCommit =
writeUpdate(
@@ -359,7 +374,8 @@
assertThat(fixedUpdateCommit.getAuthorIdent().getName())
.isEqualTo("Gerrit User " + changeOwner.getAccountId());
assertThat(originalAuthorIdent.getEmailAddress()).isEqualTo(fixedAuthorIdent.getEmailAddress());
- assertThat(originalAuthorIdent.getWhen()).isEqualTo(fixedAuthorIdent.getWhen());
+ assertThat(originalAuthorIdent.getWhen().getTime())
+ .isEqualTo(fixedAuthorIdent.getWhen().getTime());
assertThat(originalAuthorIdent.getTimeZone()).isEqualTo(fixedAuthorIdent.getTimeZone());
assertThat(invalidUpdateCommit.getFullMessage()).isEqualTo(fixedUpdateCommit.getFullMessage());
assertThat(invalidUpdateCommit.getCommitterIdent())
@@ -437,6 +453,9 @@
assertThat(secondRunResult.refsFailedToFix).isEmpty();
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Test
public void fixReviewerFooterIdent() throws Exception {
Change c = newChange();
@@ -483,7 +502,7 @@
BackfillResult result = rewriter.backfillProject(project, repo, options);
assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
- Timestamp updateTimestamp = new Timestamp(serverIdent.getWhen().getTime());
+ Instant updateTimestamp = serverIdent.getWhen().toInstant();
ImmutableList<ReviewerStatusUpdate> expectedReviewerUpdates =
ImmutableList.of(
ReviewerStatusUpdate.create(
@@ -520,6 +539,9 @@
assertThat(secondRunResult.refsFailedToFix).isEmpty();
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Test
public void fixReviewerMessage() throws Exception {
Change c = newChange();
@@ -567,21 +589,15 @@
BackfillResult result = rewriter.backfillProject(project, repo, options);
assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
- Timestamp updateTimestamp = new Timestamp(serverIdent.getWhen().getTime());
+ Instant updateTimestamp = serverIdent.getWhen().toInstant();
ImmutableList<ReviewerStatusUpdate> expectedReviewerUpdates =
ImmutableList.of(
ReviewerStatusUpdate.create(
- new Timestamp(addReviewerUpdate.when.getTime()),
- changeOwner.getAccountId(),
- otherUserId,
- REVIEWER),
+ addReviewerUpdate.when, changeOwner.getAccountId(), otherUserId, REVIEWER),
ReviewerStatusUpdate.create(
updateTimestamp, changeOwner.getAccountId(), otherUserId, REMOVED),
ReviewerStatusUpdate.create(
- new Timestamp(addCcUpdate.when.getTime()),
- changeOwner.getAccountId(),
- otherUserId,
- CC),
+ addCcUpdate.when, changeOwner.getAccountId(), otherUserId, CC),
ReviewerStatusUpdate.create(
updateTimestamp, changeOwner.getAccountId(), otherUserId, REMOVED));
ChangeNotes notesAfterRewrite = newNotes(c);
@@ -653,6 +669,9 @@
assertThat(secondRunResult.refsFailedToFix).isEmpty();
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Test
public void fixLabelFooterIdent() throws Exception {
Change c = newChange();
@@ -703,7 +722,7 @@
BackfillResult result = rewriter.backfillProject(project, repo, options);
assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
- Timestamp updateTimestamp = new Timestamp(serverIdent.getWhen().getTime());
+ Instant updateTimestamp = serverIdent.getWhen().toInstant();
ImmutableList<PatchSetApproval> expectedApprovals =
ImmutableList.of(
PatchSetApproval.builder()
@@ -787,6 +806,9 @@
assertThat(secondRunResult.refsFailedToFix).isEmpty();
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Test
public void fixRemoveVoteChangeMessage() throws Exception {
Change c = newChange();
@@ -840,7 +862,7 @@
BackfillResult result = rewriter.backfillProject(project, repo, options);
assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
- Timestamp updateTimestamp = new Timestamp(serverIdent.getWhen().getTime());
+ Instant updateTimestamp = serverIdent.getWhen().toInstant();
ImmutableList<PatchSetApproval> expectedApprovals =
ImmutableList.of(
PatchSetApproval.builder()
@@ -910,6 +932,9 @@
assertThat(secondRunResult.refsFailedToFix).isEmpty();
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Test
public void fixRemoveVoteChangeMessageWithUnparsableAuthorIdent() throws Exception {
Change c = newChange();
@@ -917,7 +942,7 @@
new PersonIdent(
changeOwner.getName(),
"server@" + serverId,
- TimeUtil.nowTs(),
+ Date.from(TimeUtil.now()),
serverIdent.getTimeZone());
writeUpdate(
RefNames.changeMetaRef(c.getId()),
@@ -1053,7 +1078,7 @@
public void fixRemoveVoteChangeMessageWithNoFooterLabel_matchDuplicateAccounts()
throws Exception {
Account duplicateCodeOwner =
- Account.builder(Account.id(4), TimeUtil.nowTs())
+ Account.builder(Account.id(4), TimeUtil.now())
.setFullName(changeOwner.getName())
.setPreferredEmail("other@test.com")
.build();
@@ -1163,6 +1188,9 @@
assertThat(secondRunResult.refsFailedToFix).isEmpty();
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Test
public void fixAttentionFooter() throws Exception {
Change c = newChange();
@@ -1243,46 +1271,46 @@
BackfillResult result = rewriter.backfillProject(project, repo, options);
assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
notesBeforeRewrite.getAttentionSetUpdates();
- Timestamp updateTimestamp = new Timestamp(serverIdent.getWhen().getTime());
+ Instant updateTimestamp = serverIdent.getWhen().toInstant();
ImmutableList<AttentionSetUpdate> attentionSetUpdatesBeforeRewrite =
ImmutableList.of(
AttentionSetUpdate.createFromRead(
- invalidRemovedByClickUpdate.getWhen().toInstant(),
+ invalidRemovedByClickUpdate.getWhen(),
changeOwner.getAccountId(),
Operation.REMOVE,
String.format("Removed by %s by clicking the attention icon", otherUser.getName())),
AttentionSetUpdate.createFromRead(
- validAttentionSetUpdate.getWhen().toInstant(),
+ validAttentionSetUpdate.getWhen(),
changeOwner.getAccountId(),
Operation.ADD,
"Added by someone"),
AttentionSetUpdate.createFromRead(
- validAttentionSetUpdate.getWhen().toInstant(),
+ validAttentionSetUpdate.getWhen(),
otherUserId,
Operation.REMOVE,
"Removed by someone"),
AttentionSetUpdate.createFromRead(
- updateTimestamp.toInstant(),
+ updateTimestamp,
changeOwner.getAccountId(),
Operation.REMOVE,
String.format("%s replied on the change", otherUser.getName())),
AttentionSetUpdate.createFromRead(
- updateTimestamp.toInstant(),
+ updateTimestamp,
otherUserId,
Operation.ADD,
"Added by someone using the hovercard menu"),
AttentionSetUpdate.createFromRead(
- invalidMultipleAttentionSetUpdate.getWhen().toInstant(),
+ invalidMultipleAttentionSetUpdate.getWhen(),
otherUserId,
Operation.REMOVE,
String.format("Removed by %s using the hovercard menu", otherUser.getName())),
AttentionSetUpdate.createFromRead(
- invalidMultipleAttentionSetUpdate.getWhen().toInstant(),
+ invalidMultipleAttentionSetUpdate.getWhen(),
changeOwner.getAccountId(),
Operation.ADD,
String.format("%s replied on the change", otherUser.getName())),
AttentionSetUpdate.createFromRead(
- invalidAttentionSetUpdate.getWhen().toInstant(),
+ invalidAttentionSetUpdate.getWhen(),
otherUserId,
Operation.ADD,
String.format("Added by %s using the hovercard menu", otherUser.getName())));
@@ -1290,42 +1318,42 @@
ImmutableList<AttentionSetUpdate> attentionSetUpdatesAfterRewrite =
ImmutableList.of(
AttentionSetUpdate.createFromRead(
- invalidRemovedByClickUpdate.getWhen().toInstant(),
+ invalidRemovedByClickUpdate.getWhen(),
changeOwner.getAccountId(),
Operation.REMOVE,
"Removed by someone by clicking the attention icon"),
AttentionSetUpdate.createFromRead(
- validAttentionSetUpdate.getWhen().toInstant(),
+ validAttentionSetUpdate.getWhen(),
changeOwner.getAccountId(),
Operation.ADD,
"Added by someone"),
AttentionSetUpdate.createFromRead(
- validAttentionSetUpdate.getWhen().toInstant(),
+ validAttentionSetUpdate.getWhen(),
otherUserId,
Operation.REMOVE,
"Removed by someone"),
AttentionSetUpdate.createFromRead(
- updateTimestamp.toInstant(),
+ updateTimestamp,
changeOwner.getAccountId(),
Operation.REMOVE,
"Someone replied on the change"),
AttentionSetUpdate.createFromRead(
- updateTimestamp.toInstant(),
+ updateTimestamp,
otherUserId,
Operation.ADD,
"Added by someone using the hovercard menu"),
AttentionSetUpdate.createFromRead(
- invalidMultipleAttentionSetUpdate.getWhen().toInstant(),
+ invalidMultipleAttentionSetUpdate.getWhen(),
otherUserId,
Operation.REMOVE,
"Removed by someone using the hovercard menu"),
AttentionSetUpdate.createFromRead(
- invalidMultipleAttentionSetUpdate.getWhen().toInstant(),
+ invalidMultipleAttentionSetUpdate.getWhen(),
changeOwner.getAccountId(),
Operation.ADD,
"Someone replied on the change"),
AttentionSetUpdate.createFromRead(
- invalidAttentionSetUpdate.getWhen().toInstant(),
+ invalidAttentionSetUpdate.getWhen(),
otherUserId,
Operation.ADD,
"Added by someone using the hovercard menu"));
@@ -1420,22 +1448,22 @@
thirdAttentionSetUpdate.commit();
attentionSetUpdatesBeforeRewrite.add(
AttentionSetUpdate.createFromRead(
- thirdAttentionSetUpdate.getWhen().toInstant(),
+ thirdAttentionSetUpdate.getWhen(),
changeOwner.getAccountId(),
Operation.REMOVE,
String.format("Removed by %s by clicking the attention icon", okAccountName)),
AttentionSetUpdate.createFromRead(
- secondAttentionSetUpdate.getWhen().toInstant(),
+ secondAttentionSetUpdate.getWhen(),
otherUserId,
Operation.REMOVE,
String.format("Removed by %s using the hovercard menu", okAccountName)),
AttentionSetUpdate.createFromRead(
- secondAttentionSetUpdate.getWhen().toInstant(),
+ secondAttentionSetUpdate.getWhen(),
changeOwner.getAccountId(),
Operation.ADD,
String.format("%s replied on the change", okAccountName)),
AttentionSetUpdate.createFromRead(
- firstAttentionSetUpdate.getWhen().toInstant(),
+ firstAttentionSetUpdate.getWhen(),
otherUserId,
Operation.ADD,
String.format("Added by %s using the hovercard menu", okAccountName)));
@@ -1541,6 +1569,9 @@
assertThat(secondRunResult.refsFailedToFix).isEmpty();
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Test
public void fixSubmitChangeMessageAndFooters() throws Exception {
Change c = newChange();
@@ -1548,7 +1579,7 @@
new PersonIdent(
changeOwner.getName(),
changeNoteUtil.getAccountIdAsEmailAddress(changeOwner.getAccountId()),
- TimeUtil.nowTs(),
+ Date.from(TimeUtil.now()),
serverIdent.getTimeZone());
String changeOwnerIdentToFix = getAccountIdentToFix(changeOwner.getAccount());
writeUpdate(
@@ -1744,16 +1775,16 @@
public void fixCodeOwnersOnAddReviewerChangeMessage() throws Exception {
Account reviewer =
- Account.builder(Account.id(3), TimeUtil.nowTs())
+ Account.builder(Account.id(3), TimeUtil.now())
.setFullName("Reviewer User")
.setPreferredEmail("reviewer@account.com")
.build();
accountCache.put(reviewer);
Account duplicateCodeOwner =
- Account.builder(Account.id(4), TimeUtil.nowTs()).setFullName(changeOwner.getName()).build();
+ Account.builder(Account.id(4), TimeUtil.now()).setFullName(changeOwner.getName()).build();
accountCache.put(duplicateCodeOwner);
Account duplicateReviewer =
- Account.builder(Account.id(5), TimeUtil.nowTs()).setFullName(reviewer.getName()).build();
+ Account.builder(Account.id(5), TimeUtil.now()).setFullName(reviewer.getName()).build();
accountCache.put(duplicateReviewer);
Change c = newChange();
ImmutableList.Builder<ObjectId> commitsToFix = new ImmutableList.Builder<>();
@@ -2211,7 +2242,7 @@
getChangeUpdateBody(c, "Assignee deleted: " + otherUser.getName()),
getAuthorIdent(changeOwner.getAccount()));
Account reviewer =
- Account.builder(Account.id(3), TimeUtil.nowTs())
+ Account.builder(Account.id(3), TimeUtil.now())
.setFullName("Reviewer User")
.setPreferredEmail("reviewer@account.com")
.build();
@@ -2250,16 +2281,19 @@
assertThat(secondRunResult.refsFailedToFix).isEmpty();
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Test
public void singleRunFixesAll() throws Exception {
Change c = newChange();
- Timestamp when = TimeUtil.nowTs();
+ Instant when = TimeUtil.now();
String assigneeIdentToFix = getAccountIdentToFix(otherUser.getAccount());
PersonIdent authorIdentToFix =
new PersonIdent(
changeOwner.getName(),
changeNoteUtil.getAccountIdAsEmailAddress(changeOwner.getAccountId()),
- when,
+ Date.from(when),
serverIdent.getTimeZone());
RevCommit invalidUpdateCommit =
@@ -2416,7 +2450,6 @@
}
private PersonIdent getAuthorIdent(Account account) {
- Timestamp when = TimeUtil.nowTs();
- return changeNoteUtil.newAccountIdIdent(account.id(), when, serverIdent);
+ return changeNoteUtil.newAccountIdIdent(account.id(), TimeUtil.now(), serverIdent);
}
}
diff --git a/javatests/com/google/gerrit/server/notedb/DraftCommentNotesTest.java b/javatests/com/google/gerrit/server/notedb/DraftCommentNotesTest.java
index 041366c..31b1db0 100644
--- a/javatests/com/google/gerrit/server/notedb/DraftCommentNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/DraftCommentNotesTest.java
@@ -89,7 +89,7 @@
0,
otherUser,
null,
- TimeUtil.nowTs(),
+ TimeUtil.now(),
"comment",
(short) 0,
ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234"),
diff --git a/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java b/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
index d47afb0..fa04cf8 100644
--- a/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
+++ b/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
@@ -17,26 +17,33 @@
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
-import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Patch.ChangeType;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.DiffOperationsTest.FileEntity.FileType;
import com.google.gerrit.server.patch.filediff.FileDiffOutput;
+import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.testing.InMemoryModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import java.io.IOException;
+import java.util.Date;
import java.util.Map;
+import java.util.Optional;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.lib.TreeFormatter;
import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.Before;
@@ -64,12 +71,15 @@
@Test
public void diffModifiedFileAgainstParent() throws Exception {
- ImmutableMap<String, String> oldFiles =
- ImmutableMap.of(fileName1, fileContent1, fileName2, fileContent2);
+ ImmutableList<FileEntity> oldFiles =
+ ImmutableList.of(
+ new FileEntity(fileName1, fileContent1), new FileEntity(fileName2, fileContent2));
ObjectId oldCommitId = createCommit(repo, null, oldFiles);
- ImmutableMap<String, String> newFiles =
- ImmutableMap.of(fileName1, fileContent1, fileName2, fileContent2 + "\nnew line here");
+ ImmutableList<FileEntity> newFiles =
+ ImmutableList.of(
+ new FileEntity(fileName1, fileContent1),
+ new FileEntity(fileName2, fileContent2 + "\nnew line here"));
ObjectId newCommitId = createCommit(repo, oldCommitId, newFiles);
FileDiffOutput diffOutput =
@@ -84,19 +94,18 @@
@Test
public void diffAgainstAutoMergePersistsAutoMergeInRepo() throws Exception {
- ObjectId parent1 = createCommit(repo, null, ImmutableMap.of("file_1.txt", "file 1 content"));
- ObjectId parent2 = createCommit(repo, null, ImmutableMap.of("file_2.txt", "file 2 content"));
+ ObjectId parent1 =
+ createCommit(repo, null, ImmutableList.of(new FileEntity("file_1.txt", "file 1 content")));
+ ObjectId parent2 =
+ createCommit(repo, null, ImmutableList.of(new FileEntity("file_2.txt", "file 2 content")));
ObjectId merge =
createMergeCommit(
repo,
- ImmutableMap.of(
- "file_1.txt",
- "file 1 content",
- "file_2.txt",
- "file 2 content",
- "file_3.txt",
- "file 3 content"),
+ ImmutableList.of(
+ new FileEntity("file_1.txt", "file 1 content"),
+ new FileEntity("file_2.txt", "file 2 content"),
+ new FileEntity("file_3.txt", "file 3 content")),
parent1,
parent2);
@@ -104,39 +113,158 @@
assertThat(repo.getRefDatabase().exactRef(autoMergeRef)).isNull();
Map<String, FileDiffOutput> changedFiles =
- diffOperations.listModifiedFilesAgainstParent(testProjectName, merge, /* parentNum=*/ 0);
+ diffOperations.listModifiedFilesAgainstParent(
+ testProjectName, merge, /* parentNum=*/ 0, DiffOptions.DEFAULTS);
assertThat(changedFiles.keySet()).containsExactly("/COMMIT_MSG", "/MERGE_LIST", "file_3.txt");
// Requesting diff against auto-merge had the side effect of updating the auto-merge ref
assertThat(repo.getRefDatabase().exactRef(autoMergeRef)).isNotNull();
}
+ @Test
+ public void loadModifiedFiles() throws Exception {
+ ImmutableList<FileEntity> oldFiles =
+ ImmutableList.of(
+ new FileEntity(fileName1, fileContent1), new FileEntity(fileName2, fileContent2));
+ ObjectId oldCommitId = createCommit(repo, null, oldFiles);
+
+ ImmutableList<FileEntity> newFiles =
+ ImmutableList.of(
+ new FileEntity(fileName1, fileContent1),
+ new FileEntity(fileName2, fileContent2 + "\nnew line here"));
+ ObjectId newCommitId = createCommit(repo, oldCommitId, newFiles);
+
+ Repository repository = repoManager.openRepository(testProjectName);
+ ObjectReader objectReader = repository.newObjectReader();
+ RevWalk rw = new RevWalk(objectReader);
+ StoredConfig repoConfig = repository.getConfig();
+
+ // This call loads modified files directly without going through the diff cache.
+ Map<String, ModifiedFile> modifiedFiles =
+ diffOperations.loadModifiedFiles(
+ testProjectName, newCommitId, oldCommitId, DiffOptions.DEFAULTS, rw, repoConfig);
+
+ assertThat(modifiedFiles)
+ .containsExactly(
+ fileName2,
+ ModifiedFile.builder()
+ .changeType(ChangeType.MODIFIED)
+ .oldPath(Optional.of(fileName2))
+ .newPath(Optional.of(fileName2))
+ .build());
+ }
+
+ @Test
+ public void loadModifiedFiles_withSymlinkConvertedToRegularFile() throws Exception {
+ // Commit 1: Create a regular fileName1 with fileContent1
+ ImmutableList<FileEntity> oldFiles = ImmutableList.of(new FileEntity(fileName1, fileContent1));
+ ObjectId oldCommitId = createCommit(repo, null, oldFiles);
+
+ // Commit 2: Create a symlink with name FileName1 pointing to target file "target"
+ ImmutableList<FileEntity> newFiles =
+ ImmutableList.of(new FileEntity(fileName1, "target", FileType.SYMLINK));
+ ObjectId newCommitId = createCommit(repo, oldCommitId, newFiles);
+
+ Repository repository = repoManager.openRepository(testProjectName);
+ ObjectReader objectReader = repository.newObjectReader();
+
+ Map<String, ModifiedFile> modifiedFiles =
+ diffOperations.loadModifiedFiles(
+ testProjectName,
+ newCommitId,
+ oldCommitId,
+ DiffOptions.DEFAULTS,
+ new RevWalk(objectReader),
+ repository.getConfig());
+
+ assertThat(modifiedFiles)
+ .containsExactly(
+ fileName1,
+ ModifiedFile.builder()
+ .changeType(ChangeType.REWRITE)
+ .oldPath(Optional.empty())
+ .newPath(Optional.of(fileName1))
+ .build());
+ }
+
+ @Test
+ public void loadModifiedFilesAgainstParent() throws Exception {
+ ImmutableList<FileEntity> oldFiles =
+ ImmutableList.of(
+ new FileEntity(fileName1, fileContent1), new FileEntity(fileName2, fileContent2));
+ ObjectId oldCommitId = createCommit(repo, null, oldFiles);
+
+ ImmutableList<FileEntity> newFiles =
+ ImmutableList.of(
+ new FileEntity(fileName1, fileContent1),
+ new FileEntity(fileName2, fileContent2 + "\nnew line here"));
+ ObjectId newCommitId = createCommit(repo, oldCommitId, newFiles);
+
+ Repository repository = repoManager.openRepository(testProjectName);
+ ObjectReader objectReader = repository.newObjectReader();
+ RevWalk rw = new RevWalk(objectReader);
+ StoredConfig repoConfig = repository.getConfig();
+
+ // This call loads modified files directly without going through the diff cache.
+ Map<String, ModifiedFile> modifiedFiles =
+ diffOperations.loadModifiedFilesAgainstParent(
+ testProjectName, newCommitId, /* parentNum=*/ 0, DiffOptions.DEFAULTS, rw, repoConfig);
+
+ assertThat(modifiedFiles)
+ .containsExactly(
+ fileName2,
+ ModifiedFile.builder()
+ .changeType(ChangeType.MODIFIED)
+ .oldPath(Optional.of(fileName2))
+ .newPath(Optional.of(fileName2))
+ .build());
+ }
+
+ static class FileEntity {
+ String name;
+ String content;
+ FileType type;
+
+ enum FileType {
+ REGULAR,
+ SYMLINK
+ }
+
+ FileEntity(String name, String content) {
+ this(name, content, FileType.REGULAR);
+ }
+
+ FileEntity(String name, String content, FileType type) {
+ this.name = name;
+ this.content = content;
+ this.type = type;
+ }
+ }
+
private ObjectId createMergeCommit(
- Repository repo,
- ImmutableMap<String, String> fileNameToContent,
- ObjectId parent1,
- ObjectId parent2)
+ Repository repo, ImmutableList<FileEntity> fileEntities, ObjectId parent1, ObjectId parent2)
throws IOException {
- ObjectId treeId = createTree(repo, fileNameToContent);
+ ObjectId treeId = createTree(repo, fileEntities);
return createCommitInRepo(repo, treeId, parent1, parent2);
}
private ObjectId createCommit(
- Repository repo,
- @Nullable ObjectId parentCommit,
- ImmutableMap<String, String> fileNameToContent)
+ Repository repo, @Nullable ObjectId parentCommit, ImmutableList<FileEntity> fileEntities)
throws IOException {
- ObjectId treeId = createTree(repo, fileNameToContent);
+ ObjectId treeId = createTree(repo, fileEntities);
return parentCommit == null
? createCommitInRepo(repo, treeId)
: createCommitInRepo(repo, treeId, parentCommit);
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
private static ObjectId createCommitInRepo(Repository repo, ObjectId treeId, ObjectId... parents)
throws IOException {
try (ObjectInserter oi = repo.newObjectInserter()) {
PersonIdent committer =
- new PersonIdent(new PersonIdent("Foo Bar", "foo.bar@baz.com"), TimeUtil.nowTs());
+ new PersonIdent(new PersonIdent("Foo Bar", "foo.bar@baz.com"), Date.from(TimeUtil.now()));
CommitBuilder cb = new CommitBuilder();
cb.setTreeId(treeId);
cb.setCommitter(committer);
@@ -152,17 +280,21 @@
}
}
- private static ObjectId createTree(
- Repository repo, ImmutableMap<String, String> fileNameToContent) throws IOException {
+ private static ObjectId createTree(Repository repo, ImmutableList<FileEntity> fileEntities)
+ throws IOException {
try (ObjectInserter oi = repo.newObjectInserter();
ObjectReader reader = repo.newObjectReader();
RevWalk rw = new RevWalk(reader); ) {
TreeFormatter formatter = new TreeFormatter();
- for (Map.Entry<String, String> entry : fileNameToContent.entrySet()) {
- String fileName = entry.getKey();
- String fileContent = entry.getValue();
+ for (FileEntity fileEntity : fileEntities) {
+ String fileName = fileEntity.name;
+ String fileContent = fileEntity.content;
ObjectId fileObjId = createBlob(repo, fileContent);
- formatter.append(fileName, rw.lookupBlob(fileObjId));
+ if (fileEntity.type.equals(FileType.REGULAR)) {
+ formatter.append(fileName, rw.lookupBlob(fileObjId));
+ } else {
+ formatter.append(fileName, FileMode.SYMLINK, fileObjId);
+ }
}
ObjectId treeId = oi.insert(formatter);
oi.flush();
diff --git a/javatests/com/google/gerrit/server/patch/MagicFileTest.java b/javatests/com/google/gerrit/server/patch/MagicFileTest.java
index 93928f0..21ea641 100644
--- a/javatests/com/google/gerrit/server/patch/MagicFileTest.java
+++ b/javatests/com/google/gerrit/server/patch/MagicFileTest.java
@@ -95,6 +95,9 @@
assertThat(magicFile.getStartLineOfModifiableContent()).isEqualTo(1);
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Test
public void commitMessageFileOfRootCommitContainsCorrectContent() throws Exception {
try (Repository repository = repositoryManager.createRepository(Project.nameKey("repo1"));
@@ -146,6 +149,9 @@
}
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Test
public void commitMessageFileOfNonMergeCommitContainsCorrectContent() throws Exception {
try (Repository repository = repositoryManager.createRepository(Project.nameKey("repo1"));
@@ -202,6 +208,9 @@
}
}
+ // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+ // Instants
+ @SuppressWarnings("JdkObsolete")
@Test
public void commitMessageFileOfMergeCommitContainsCorrectContent() throws Exception {
try (Repository repository = repositoryManager.createRepository(Project.nameKey("repo1"));
diff --git a/javatests/com/google/gerrit/server/plugins/AutoRegisterModulesTest.java b/javatests/com/google/gerrit/server/plugins/AutoRegisterModulesTest.java
index 55c9bc3..ca4872d 100644
--- a/javatests/com/google/gerrit/server/plugins/AutoRegisterModulesTest.java
+++ b/javatests/com/google/gerrit/server/plugins/AutoRegisterModulesTest.java
@@ -27,11 +27,11 @@
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
-import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.jar.Manifest;
+import java.util.stream.Stream;
import org.junit.Test;
public class AutoRegisterModulesTest {
@@ -94,7 +94,7 @@
}
@Override
- public Enumeration<PluginEntry> entries() {
+ public Stream<PluginEntry> entries() {
return null;
}
}
diff --git a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
index 9df59c2..2824bb1 100644
--- a/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
+++ b/javatests/com/google/gerrit/server/project/ProjectConfigTest.java
@@ -217,7 +217,7 @@
"[submit-requirement \"Code-review\"]\n"
+ " description = At least one Code Review +2\n"
+ " applicableIf =branch(refs/heads/master)\n"
- + " submittableIf = label(code-review, +2)\n"
+ + " submittableIf = label(Code-Review, +2)\n"
+ "[submit-requirement \"api-review\"]\n"
+ " description = Additional review required for API modifications\n"
+ " applicableIf =commit_filepath_contains(\\\"/api/.*\\\")\n"
@@ -237,7 +237,7 @@
.setApplicabilityExpression(
SubmitRequirementExpression.of("branch(refs/heads/master)"))
.setSubmittabilityExpression(
- SubmitRequirementExpression.create("label(code-review, +2)"))
+ SubmitRequirementExpression.create("label(Code-Review, +2)"))
.setOverrideExpression(Optional.empty())
.setAllowOverrideInChildProjects(false)
.build(),
@@ -262,19 +262,19 @@
.add("groups", group(developers))
.add(
"project.config",
- "[submit-requirement \"code-review\"]\n"
- + " submittableIf = label(code-review, +2)\n")
+ "[submit-requirement \"Code-Review\"]\n"
+ + " submittableIf = label(Code-Review, +2)\n")
.create();
ProjectConfig cfg = read(rev);
Map<String, SubmitRequirement> submitRequirements = cfg.getSubmitRequirementSections();
assertThat(submitRequirements)
.containsExactly(
- "code-review",
+ "Code-Review",
SubmitRequirement.builder()
- .setName("code-review")
+ .setName("Code-Review")
.setSubmittabilityExpression(
- SubmitRequirementExpression.create("label(code-review, +2)"))
+ SubmitRequirementExpression.create("label(Code-Review, +2)"))
.setAllowOverrideInChildProjects(false)
.build());
}
@@ -320,8 +320,8 @@
.add("groups", group(developers))
.add(
"project.config",
- "[submit-requirement \"code-review\"]\n"
- + " applicableIf =label(code-review, +2)\n")
+ "[submit-requirement \"Code-Review\"]\n"
+ + " applicableIf =label(Code-Review, +2)\n")
.create();
ProjectConfig cfg = read(rev);
@@ -330,8 +330,9 @@
assertThat(cfg.getValidationErrors()).hasSize(1);
assertThat(Iterables.getOnlyElement(cfg.getValidationErrors()).getMessage())
.isEqualTo(
- "project.config: Submit requirement 'code-review' does not define a submittability"
- + " expression.");
+ "project.config: Setting a submittability expression for submit requirement"
+ + " 'Code-Review' is required: Missing"
+ + " submit-requirement.Code-Review.submittableIf");
}
@Test
@@ -395,6 +396,31 @@
}
@Test
+ public void readConfigLabelInvalidBranchPattern() throws Exception {
+ RevCommit rev =
+ tr.commit()
+ .add("groups", group(developers))
+ .add(
+ "project.config",
+ "[label \"CustomLabel\"]\n"
+ + " value = -1 Negative\n"
+ + " value = 0 No Score\n"
+ + " value = 1 Positive\n"
+ + " branch = ^***\n"
+ + " defaultValue = 0\n")
+ .create();
+
+ ProjectConfig cfg = read(rev);
+ assertThat(cfg.getValidationErrors()).hasSize(1);
+ assertThat(Iterables.getOnlyElement(cfg.getValidationErrors()).getMessage())
+ .isEqualTo(
+ "project.config: Invalid ref pattern \"^***\""
+ + " in label.CustomLabel.branch: Dangling meta character '*' near index 2\n"
+ + "^***\n"
+ + " ^");
+ }
+
+ @Test
public void readConfigLabelScores() throws Exception {
RevCommit rev =
tr.commit()
@@ -604,7 +630,7 @@
PluginConfig pluginCfg = cfg.getPluginConfig("somePlugin");
assertThat(pluginCfg.getNames()).hasSize(2);
assertThat(pluginCfg.getString("key1")).isEqualTo("value1");
- assertThat(pluginCfg.getStringList(("key2"))).isEqualTo(new String[] {"value2a", "value2b"});
+ assertThat(pluginCfg.getStringList("key2")).isEqualTo(new String[] {"value2a", "value2b"});
}
@Test
@@ -952,10 +978,10 @@
tr.commit()
.add(
"project.config",
- "[submit-requirement \"code-review\"]\n"
+ "[submit-requirement \"Code-Review\"]\n"
+ " description = At least one Code Review +2\n"
+ " applicableIf =branch(refs/heads/master)\n"
- + " submittableIf = label(code-review, +2)\n"
+ + " submittableIf = label(Code-Review, +2)\n"
+ "[notify \"name\"]\n"
+ " email = example@example.com\n")
.create();
diff --git a/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java b/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java
index 05eb6e0..45ccb03 100644
--- a/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java
+++ b/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java
@@ -94,7 +94,8 @@
createLabel("Verified", Label.Status.OK)));
List<SubmitRequirementResult> requirements =
- SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId);
+ SubmitRequirementsAdapter.createResult(
+ submitRecord, labelTypes, psCommitId, /* isForced= */ false);
assertThat(requirements).hasSize(2);
assertResult(
@@ -122,7 +123,8 @@
createLabel("Verified", Label.Status.NEED)));
List<SubmitRequirementResult> requirements =
- SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId);
+ SubmitRequirementsAdapter.createResult(
+ submitRecord, labelTypes, psCommitId, /* isForced= */ false);
assertThat(requirements).hasSize(2);
assertResult(
@@ -140,6 +142,32 @@
}
@Test
+ public void defaultSubmitRule_withOneLabelForced() {
+ SubmitRecord submitRecord =
+ createSubmitRecord(
+ "gerrit~DefaultSubmitRule",
+ Status.OK,
+ Arrays.asList(createLabel("Code-Review", Label.Status.NEED)));
+
+ // Submit records that are forced are written with their initial status in NoteDb, e.g. NEED.
+ // If we do a force submit, the gerrit server appends an extra marker record with status=FORCED
+ // to indicate that all other records were forced, that's why we explicitly pass isForced=true
+ // to the "submit requirements adapter". The resulting submit requirement result has a
+ // status=FORCED.
+ List<SubmitRequirementResult> requirements =
+ SubmitRequirementsAdapter.createResult(
+ submitRecord, labelTypes, psCommitId, /* isForced= */ true);
+
+ assertThat(requirements).hasSize(1);
+ assertResult(
+ requirements.get(0),
+ /* reqName= */ "Code-Review",
+ /* submitExpression= */ "label:Code-Review=MAX -label:Code-Review=MIN",
+ SubmitRequirementResult.Status.FORCED,
+ SubmitRequirementExpressionResult.Status.FAIL);
+ }
+
+ @Test
public void defaultSubmitRule_withLabelStatusNeed_labelHasIgnoreSelfApproval() throws Exception {
SubmitRecord submitRecord =
createSubmitRecord(
@@ -148,7 +176,8 @@
Arrays.asList(createLabel("ISA-Label", Label.Status.NEED)));
List<SubmitRequirementResult> requirements =
- SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId);
+ SubmitRequirementsAdapter.createResult(
+ submitRecord, labelTypes, psCommitId, /* isForced= */ false);
assertThat(requirements).hasSize(1);
assertResult(
@@ -168,7 +197,8 @@
Arrays.asList(createLabel("ISA-Label", Label.Status.OK)));
List<SubmitRequirementResult> requirements =
- SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId);
+ SubmitRequirementsAdapter.createResult(
+ submitRecord, labelTypes, psCommitId, /* isForced= */ false);
assertThat(requirements).hasSize(1);
assertResult(
@@ -180,12 +210,52 @@
}
@Test
+ public void defaultSubmitRule_withNonExistingLabel() throws Exception {
+ SubmitRecord submitRecord =
+ createSubmitRecord(
+ "gerrit~DefaultSubmitRule",
+ Status.OK,
+ Arrays.asList(createLabel("Non-Existing", Label.Status.OK)));
+
+ List<SubmitRequirementResult> requirements =
+ SubmitRequirementsAdapter.createResult(
+ submitRecord, labelTypes, psCommitId, /* isForced= */ false);
+
+ assertThat(requirements).isEmpty();
+ }
+
+ @Test
+ public void defaultSubmitRule_withExistingAndNonExistingLabels() throws Exception {
+ SubmitRecord submitRecord =
+ createSubmitRecord(
+ "gerrit~DefaultSubmitRule",
+ Status.OK,
+ Arrays.asList(
+ createLabel("Non-Existing", Label.Status.OK),
+ createLabel("Code-Review", Label.Status.OK)));
+
+ List<SubmitRequirementResult> requirements =
+ SubmitRequirementsAdapter.createResult(
+ submitRecord, labelTypes, psCommitId, /* isForced= */ false);
+
+ // The "Non-Existing" label was skipped since it does not exist in the project config.
+ assertThat(requirements).hasSize(1);
+ assertResult(
+ requirements.get(0),
+ /* reqName= */ "Code-Review",
+ /* submitExpression= */ "label:Code-Review=MAX -label:Code-Review=MIN",
+ SubmitRequirementResult.Status.SATISFIED,
+ SubmitRequirementExpressionResult.Status.PASS);
+ }
+
+ @Test
public void customSubmitRule_noLabels_withStatusOk() {
SubmitRecord submitRecord =
createSubmitRecord("gerrit~IgnoreSelfApprovalRule", Status.OK, Arrays.asList());
List<SubmitRequirementResult> requirements =
- SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId);
+ SubmitRequirementsAdapter.createResult(
+ submitRecord, labelTypes, psCommitId, /* isForced= */ false);
assertThat(requirements).hasSize(1);
assertResult(
@@ -202,7 +272,8 @@
createSubmitRecord("gerrit~IgnoreSelfApprovalRule", Status.OK, /* labels= */ null);
List<SubmitRequirementResult> requirements =
- SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId);
+ SubmitRequirementsAdapter.createResult(
+ submitRecord, labelTypes, psCommitId, /* isForced= */ false);
assertThat(requirements).hasSize(1);
assertResult(
@@ -219,7 +290,8 @@
createSubmitRecord("gerrit~IgnoreSelfApprovalRule", Status.NOT_READY, Arrays.asList());
List<SubmitRequirementResult> requirements =
- SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId);
+ SubmitRequirementsAdapter.createResult(
+ submitRecord, labelTypes, psCommitId, /* isForced= */ false);
assertThat(requirements).hasSize(1);
assertResult(
@@ -241,7 +313,8 @@
createLabel("custom-label-2", Label.Status.REJECT)));
List<SubmitRequirementResult> requirements =
- SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId);
+ SubmitRequirementsAdapter.createResult(
+ submitRecord, labelTypes, psCommitId, /* isForced= */ false);
assertThat(requirements).hasSize(2);
assertResult(
@@ -269,7 +342,8 @@
createLabel("custom-label-2", Label.Status.REJECT)));
List<SubmitRequirementResult> requirements =
- SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId);
+ SubmitRequirementsAdapter.createResult(
+ submitRecord, labelTypes, psCommitId, /* isForced= */ false);
assertThat(requirements).hasSize(2);
assertResult(
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 9acce8f..55340e3 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -26,6 +26,7 @@
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.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
@@ -40,6 +41,9 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
import com.google.common.collect.Streams;
import com.google.common.truth.ThrowableSubject;
import com.google.gerrit.acceptance.ExtensionRegistry;
@@ -48,6 +52,7 @@
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.BranchNameKey;
@@ -111,6 +116,7 @@
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.PatchSetInserter;
import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
@@ -136,11 +142,11 @@
import com.google.inject.Provider;
import java.io.IOException;
import java.sql.Timestamp;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
-import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@@ -918,6 +924,7 @@
@Test
public void byTopic() throws Exception {
+
TestRepository<Repo> repo = createProject("repo");
ChangeInserter ins1 = newChangeWithTopic(repo, "feature1");
Change change1 = insert(repo, ins1);
@@ -948,6 +955,11 @@
assertQuery("intopic:gerrit", change6, change5);
assertQuery("topic:\"\"", change_no_topic);
assertQuery("intopic:\"\"", change_no_topic);
+
+ assume().that(getSchema().hasField(ChangeField.PREFIX_TOPIC)).isTrue();
+ assertQuery("prefixtopic:feature", change4, change2, change1);
+ assertQuery("prefixtopic:Cher", change3);
+ assertQuery("prefixtopic:feature22");
}
@Test
@@ -1037,6 +1049,7 @@
ChangeInserter ins3 = newChange(repo);
ChangeInserter ins4 = newChange(repo);
ChangeInserter ins5 = newChange(repo);
+ ChangeInserter ins6 = newChange(repo);
Change reviewMinus2Change = insert(repo, ins);
gApi.changes().id(reviewMinus2Change.getId().get()).current().review(ReviewInput.reject());
@@ -1049,7 +1062,13 @@
Change reviewPlus1Change = insert(repo, ins4);
gApi.changes().id(reviewPlus1Change.getId().get()).current().review(ReviewInput.recommend());
- Change reviewPlus2Change = insert(repo, ins5);
+ Change reviewTwoPlus1Change = insert(repo, ins5);
+ gApi.changes().id(reviewTwoPlus1Change.getId().get()).current().review(ReviewInput.recommend());
+ requestContext.setContext(newRequestContext(createAccount("user1")));
+ gApi.changes().id(reviewTwoPlus1Change.getId().get()).current().review(ReviewInput.recommend());
+ requestContext.setContext(newRequestContext(userId));
+
+ Change reviewPlus2Change = insert(repo, ins6);
gApi.changes().id(reviewPlus2Change.getId().get()).current().review(ReviewInput.approve());
Map<String, Short> m =
@@ -1060,8 +1079,10 @@
assertThat(m).hasSize(1);
assertThat(m).containsEntry("Code-Review", Short.valueOf((short) 1));
- Map<Integer, Change> changes = new LinkedHashMap<>(5);
+ Multimap<Integer, Change> changes =
+ Multimaps.newListMultimap(Maps.newLinkedHashMap(), () -> Lists.newArrayList());
changes.put(2, reviewPlus2Change);
+ changes.put(1, reviewTwoPlus1Change);
changes.put(1, reviewPlus1Change);
changes.put(0, noLabelChange);
changes.put(-1, reviewMinus1Change);
@@ -1073,9 +1094,9 @@
assertQuery("label:Code-Review=-1", reviewMinus1Change);
assertQuery("label:Code-Review-1", reviewMinus1Change);
assertQuery("label:Code-Review=0", noLabelChange);
- assertQuery("label:Code-Review=+1", reviewPlus1Change);
- assertQuery("label:Code-Review=1", reviewPlus1Change);
- assertQuery("label:Code-Review+1", reviewPlus1Change);
+ assertQuery("label:Code-Review=+1", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=1", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review+1", reviewTwoPlus1Change, reviewPlus1Change);
assertQuery("label:Code-Review=+2", reviewPlus2Change);
assertQuery("label:Code-Review=2", reviewPlus2Change);
assertQuery("label:Code-Review+2", reviewPlus2Change);
@@ -1083,6 +1104,7 @@
assertQuery(
"label:Code-Review=ANY",
reviewPlus2Change,
+ reviewTwoPlus1Change,
reviewPlus1Change,
reviewMinus1Change,
reviewMinus2Change);
@@ -1111,14 +1133,70 @@
assertQuery("label:Code-Review<-2");
assertQuery("label:Code-Review=+1,anotheruser");
- assertQuery("label:Code-Review=+1,user", reviewPlus1Change);
- assertQuery("label:Code-Review=+1,user=user", reviewPlus1Change);
- assertQuery("label:Code-Review=+1,Administrators", reviewPlus1Change);
- assertQuery("label:Code-Review=+1,group=Administrators", reviewPlus1Change);
- assertQuery("label:Code-Review=+1,user=owner", reviewPlus1Change);
- assertQuery("label:Code-Review=+1,owner", reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,user", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,user=user", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,Administrators", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery(
+ "label:Code-Review=+1,group=Administrators", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,user=owner", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,owner", reviewTwoPlus1Change, reviewPlus1Change);
assertQuery("label:Code-Review=+2,owner", reviewPlus2Change);
assertQuery("label:Code-Review=-2,owner", reviewMinus2Change);
+
+ // count=0 is not allowed
+ Exception thrown =
+ assertThrows(BadRequestException.class, () -> assertQuery("label:Code-Review=+2,count=0"));
+ assertThat(thrown).hasMessageThat().isEqualTo("Argument count=0 is not allowed.");
+ assertQuery("label:Code-Review=1,count=1", reviewPlus1Change);
+ assertQuery("label:Code-Review=1,count=2", reviewTwoPlus1Change);
+ assertQuery("label:Code-Review=1,count>=2", reviewTwoPlus1Change);
+ assertQuery("label:Code-Review=1,count>1", reviewTwoPlus1Change);
+ assertQuery("label:Code-Review=1,count>=1", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=1,count=3");
+ thrown =
+ assertThrows(BadRequestException.class, () -> assertQuery("label:Code-Review=1,count=7"));
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo("count=7 is not allowed. Maximum allowed value for count is 5.");
+
+ // Less than operator does not match with changes having count=0 for a specific vote value (i.e.
+ // no votes for that specific value). We do that deliberately since the computation of count=0
+ // for label values is expensive when the change is re-indexed. This is because the operation
+ // requires generating all formats for all {label-type, vote}=0 values that are non-voted for
+ // the change and storing them with the 'count=0' format.
+ assertQuery("label:Code-Review=1,count<5", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=1,count<=5", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery(
+ "label:Code-Review=1,count<=1", // reviewTwoPlus1Change is not matched since its count=2
+ reviewPlus1Change);
+ assertQuery(
+ "label:Code-Review=1,count<5 label:Code-Review=1,count>=1",
+ reviewTwoPlus1Change,
+ reviewPlus1Change);
+ assertQuery(
+ "label:Code-Review=1,count<=5 label:Code-Review=1,count>=1",
+ reviewTwoPlus1Change,
+ reviewPlus1Change);
+ assertQuery("label:Code-Review=1,count<=1 label:Code-Review=1,count>=1", reviewPlus1Change);
+
+ assertQuery("label:Code-Review=MAX,count=1", reviewPlus2Change);
+ assertQuery("label:Code-Review=MAX,count=2");
+ assertQuery("label:Code-Review=MIN,count=1", reviewMinus2Change);
+ assertQuery("label:Code-Review=MIN,count>1");
+ assertQuery("label:Code-Review=MAX,count<2", reviewPlus2Change);
+ assertQuery("label:Code-Review=MIN,count<1");
+ assertQuery("label:Code-Review=MAX,count<2 label:Code-Review=MAX,count>=1", reviewPlus2Change);
+ assertQuery("label:Code-Review=MIN,count<1 label:Code-Review=MIN,count>=1");
+ assertQuery("label:Code-Review>=+1,count=2", reviewTwoPlus1Change);
+
+ // "count" and "user" args cannot be used simultaneously.
+ assertThrows(
+ BadRequestException.class,
+ () -> assertQuery("label:Code-Review=+1,user=non_uploader,count=2"));
+
+ // "count" and "group" args cannot be used simultaneously.
+ assertThrows(
+ BadRequestException.class, () -> assertQuery("label:Code-Review=+1,group=gerrit,count=2"));
}
@Test
@@ -1223,16 +1301,15 @@
assertQuery("label:Code-Review=+1,non_uploader", reviewPlus1Change);
}
- private Change[] codeReviewInRange(Map<Integer, Change> changes, int start, int end) {
- int size = 0;
- Change[] range = new Change[end - start + 1];
- for (int i : changes.keySet()) {
+ private Change[] codeReviewInRange(Multimap<Integer, Change> changes, int start, int end) {
+ List<Change> range = new ArrayList<>();
+ for (Map.Entry<Integer, Change> entry : changes.entries()) {
+ int i = entry.getKey();
if (i >= start && i <= end) {
- range[size] = changes.get(i);
- size++;
+ range.add(entry.getValue());
}
}
- return range;
+ return range.toArray(new Change[0]);
}
private String createGroup(String name, String owner) throws Exception {
@@ -1705,8 +1782,9 @@
resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
TestRepository<Repo> repo = createProject("repo");
long startMs = TestTimeUtil.START.toEpochMilli();
- Change change1 = insert(repo, newChange(repo), null, new Timestamp(startMs));
- Change change2 = insert(repo, newChange(repo), null, new Timestamp(startMs + thirtyHoursInMs));
+ Change change1 = insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs));
+ Change change2 =
+ insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
// Stop time so age queries use the same endpoint.
TestTimeUtil.setClockStep(0, MILLISECONDS);
@@ -1745,8 +1823,9 @@
resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
TestRepository<Repo> repo = createProject("repo");
long startMs = TestTimeUtil.START.toEpochMilli();
- Change change1 = insert(repo, newChange(repo), null, new Timestamp(startMs));
- Change change2 = insert(repo, newChange(repo), null, new Timestamp(startMs + thirtyHoursInMs));
+ Change change1 = insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs));
+ Change change2 =
+ insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
TestTimeUtil.setClockStep(0, MILLISECONDS);
// Change1 was last updated on 2009-09-30 21:00:00 -0000
@@ -1796,8 +1875,9 @@
resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
TestRepository<Repo> repo = createProject("repo");
long startMs = TestTimeUtil.START.toEpochMilli();
- Change change1 = insert(repo, newChange(repo), null, new Timestamp(startMs));
- Change change2 = insert(repo, newChange(repo), null, new Timestamp(startMs + thirtyHoursInMs));
+ Change change1 = insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs));
+ Change change2 =
+ insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
TestTimeUtil.setClockStep(0, MILLISECONDS);
// Change1 was last updated on 2009-09-30 21:00:00 -0000
@@ -2107,6 +2187,15 @@
}
@Test
+ public void byHashtagPrefix() throws Exception {
+ assume().that(getSchema().hasField(ChangeField.PREFIX_HASHTAG)).isTrue();
+ List<Change> changes = setUpHashtagChanges();
+ assertQuery("prefixhashtag:a", changes.get(1), changes.get(0));
+ assertQuery("prefixhashtag:aa", changes.get(0));
+ assertQuery("prefixhashtag:bar", changes.get(1));
+ }
+
+ @Test
public void byHashtagRegex() throws Exception {
TestRepository<Repo> repo = createProject("repo");
Change change1 = insert(repo, newChange(repo));
@@ -2358,7 +2447,21 @@
}
@Test
- public void byHasDraft() throws Exception {
+ public void byHasDraft_draftsComputedFromIndex() throws Exception {
+ byHasDraft();
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY)
+ public void byHasDraft_draftsComputedFromAllUsersRepository() throws Exception {
+ byHasDraft();
+ }
+
+ private void byHasDraft() throws Exception {
TestRepository<Repo> repo = createProject("repo");
Change change1 = insert(repo, newChange(repo));
Change change2 = insert(repo, newChange(repo));
@@ -2386,7 +2489,11 @@
assertQuery("has:draft");
}
- @Test
+ /**
+ * This test does not have a test about drafts computed from All-Users Repository because zombie
+ * drafts can't be filtered when computing from All-Users repository. TODO(paiking): During
+ * rollout, we should find a way to fix zombie drafts.
+ */
public void byHasDraftExcludesZombieDrafts() throws Exception {
Project.NameKey project = Project.nameKey("repo");
TestRepository<Repo> repo = createProject(project.get());
@@ -2424,8 +2531,62 @@
assertQuery("has:draft");
}
+ public void byHasDraftWithManyDrafts_draftsComputedFromIndex() throws Exception {
+ byHasDraftWithManyDrafts();
+ }
+
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY)
+ public void byHasDraftWithManyDrafts_draftsComputedFromAllUsersRepository() throws Exception {
+ byHasDraftWithManyDrafts();
+ }
+
+ private void byHasDraftWithManyDrafts() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ Change[] changesWithDrafts = new Change[30];
+
+ // unrelated change not shown in the result.
+ insert(repo, newChange(repo));
+
+ for (int i = 0; i < changesWithDrafts.length; i++) {
+ // put the changes in reverse order since this is the order we receive them from the index.
+ changesWithDrafts[changesWithDrafts.length - 1 - i] = insert(repo, newChange(repo));
+ DraftInput in = new DraftInput();
+ in.line = 1;
+ in.message = "nit: trailing whitespace";
+ in.path = Patch.COMMIT_MSG;
+ gApi.changes()
+ .id(changesWithDrafts[changesWithDrafts.length - 1 - i].getId().get())
+ .current()
+ .createDraft(in);
+ }
+ assertQuery("has:draft", changesWithDrafts);
+
+ Account.Id user2 =
+ accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
+ requestContext.setContext(newRequestContext(user2));
+ assertQuery("has:draft");
+ }
+
@Test
- public void byStarredBy() throws Exception {
+ public void byStarredBy_starsComputedFromIndex() throws Exception {
+ byStarredBy();
+ }
+
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY)
+ @Test
+ public void byStarredBy_starsComputedFromAllUsersRepository() throws Exception {
+ byStarredBy();
+ }
+
+ private void byStarredBy() throws Exception {
TestRepository<Repo> repo = createProject("repo");
Change change1 = insert(repo, newChange(repo));
Change change2 = insert(repo, newChange(repo));
@@ -2446,7 +2607,21 @@
}
@Test
- public void byStar() throws Exception {
+ public void byStar_starsComputedFromIndex() throws Exception {
+ byStar();
+ }
+
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY)
+ @Test
+ public void byStar_starsComputedFromAllUsersRepository() throws Exception {
+ byStar();
+ }
+
+ private void byStar() throws Exception {
TestRepository<Repo> repo = createProject("repo");
Change change1 = insert(repo, newChangeWithStatus(repo, Change.Status.MERGED));
Change change2 = insert(repo, newChangeWithStatus(repo, Change.Status.MERGED));
@@ -2472,7 +2647,21 @@
}
@Test
- public void byIgnore() throws Exception {
+ public void byIgnore_starsComputedFromIndex() throws Exception {
+ byIgnore();
+ }
+
+ @Test
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY)
+ public void byIgnore_starsComputedFromAllUsersRepository() throws Exception {
+ byIgnore();
+ }
+
+ private void byIgnore() throws Exception {
TestRepository<Repo> repo = createProject("repo");
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
@@ -2492,6 +2681,42 @@
assertQuery("-star:ignore", change2, change1);
}
+ public void byStarWithManyStars_starsComputedFromIndex() throws Exception {
+ byStarWithManyStars();
+ }
+
+ @GerritConfig(
+ name = "experiments.enabled",
+ value =
+ ExperimentFeaturesConstants
+ .GERRIT_BACKEND_REQUEST_FEATURE_COMPUTE_FROM_ALL_USERS_REPOSITORY)
+ public void byStarWithManyStars_starsComputedFromAllUsersRepository() throws Exception {
+ byStarWithManyStars();
+ }
+
+ private void byStarWithManyStars() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ Change[] changesWithDrafts = new Change[30];
+ for (int i = 0; i < changesWithDrafts.length; i++) {
+ // put the changes in reverse order since this is the order we receive them from the index.
+ changesWithDrafts[changesWithDrafts.length - 1 - i] = insert(repo, newChange(repo));
+
+ // star the change
+ gApi.accounts()
+ .self()
+ .starChange(changesWithDrafts[changesWithDrafts.length - 1 - i].getId().toString());
+
+ // ignore the change
+ gApi.changes()
+ .id(changesWithDrafts[changesWithDrafts.length - 1 - i].getId().toString())
+ .ignore(true);
+ }
+
+ // all changes are both starred and ignored.
+ assertQuery("is:ignored", changesWithDrafts);
+ assertQuery("is:starred", changesWithDrafts);
+ }
+
@Test
public void byFrom() throws Exception {
TestRepository<Repo> repo = createProject("repo");
@@ -2824,8 +3049,6 @@
assertQuery("is:submittable", change1);
assertQuery("-is:submittable", change2);
- assertQuery("submittable:ok", change1);
- assertQuery("submittable:not_ready", change2);
assertQuery("label:CodE-RevieW=ok", change1);
assertQuery("label:CodE-RevieW=ok,user=user", change1);
@@ -2839,8 +3062,8 @@
assertQuery("label:CodE-RevieW=need,user");
gApi.changes().id(change1.getId().get()).current().submit();
- assertQuery("submittable:ok");
- assertQuery("submittable:closed", change1);
+ assertQuery("is:submittable");
+ assertQuery("-is:submittable", change1, change2);
}
@Test
@@ -3754,6 +3977,33 @@
}
@Test
+ public void isPureRevert() throws Exception {
+ assume().that(getSchema().hasField(ChangeField.IS_PURE_REVERT)).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));
+ gApi.changes().id(initial.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(initial.getChangeId()).current().submit();
+
+ ChangeInfo changeToRevert =
+ gApi.changes().create(new ChangeInput("repo", "master", "commit to revert")).get();
+ gApi.changes().id(changeToRevert.id).current().review(ReviewInput.approve());
+ gApi.changes().id(changeToRevert.id).current().submit();
+
+ ChangeInfo changeThatReverts = gApi.changes().id(changeToRevert.id).revert().get();
+ Change.Id changeThatRevertsId = Change.id(changeThatReverts._number);
+ assertQueryByIds("is:pure-revert", changeThatRevertsId);
+
+ // Update the change that reverts such that it's not a pure revert
+ gApi.changes()
+ .id(changeThatReverts.id)
+ .edit()
+ .modifyFile("some-file.txt", RawInputUtil.create("newcontent".getBytes(UTF_8)));
+ gApi.changes().id(changeThatReverts.id).edit().publish();
+ assertQueryByIds("is:pure-revert");
+ }
+
+ @Test
public void selfFailsForAnonymousUser() throws Exception {
for (String query : ImmutableList.of("assignee:self", "has:star", "is:starred", "star:star")) {
assertQuery(query);
@@ -3903,19 +4153,16 @@
}
protected Change insert(TestRepository<Repo> repo, ChangeInserter ins) throws Exception {
- return insert(repo, ins, null, TimeUtil.nowTs());
+ return insert(repo, ins, null, TimeUtil.now());
}
protected Change insert(TestRepository<Repo> repo, ChangeInserter ins, @Nullable Account.Id owner)
throws Exception {
- return insert(repo, ins, owner, TimeUtil.nowTs());
+ return insert(repo, ins, owner, TimeUtil.now());
}
protected Change insert(
- TestRepository<Repo> repo,
- ChangeInserter ins,
- @Nullable Account.Id owner,
- Timestamp createdOn)
+ TestRepository<Repo> repo, ChangeInserter ins, @Nullable Account.Id owner, Instant createdOn)
throws Exception {
Project.NameKey project =
Project.nameKey(repo.getRepository().getDescription().getRepositoryName());
@@ -3941,7 +4188,7 @@
.create(changeNotesFactory.createChecked(c), PatchSet.id(c.getId(), n), commit)
.setFireRevisionCreated(false)
.setValidate(false);
- try (BatchUpdate bu = updateFactory.create(c.getProject(), user, TimeUtil.nowTs());
+ try (BatchUpdate bu = updateFactory.create(c.getProject(), user, TimeUtil.now());
ObjectInserter oi = repo.getRepository().newObjectInserter();
ObjectReader reader = oi.newReader();
RevWalk rw = new RevWalk(reader)) {
@@ -4080,7 +4327,7 @@
}
protected static long lastUpdatedMs(Change c) {
- return c.getLastUpdatedOn().getTime();
+ return c.getLastUpdatedOn().toEpochMilli();
}
// Get the last updated time from ChangeApi
diff --git a/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java b/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
index e42230f..e48d4af 100644
--- a/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
+++ b/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
@@ -46,7 +46,7 @@
.id(PatchSet.id(changeId, num))
.commitId(ObjectId.zeroId())
.uploader(Account.id(1234))
- .createdOn(TimeUtil.nowTs())
+ .createdOn(TimeUtil.now())
.build();
}
}
diff --git a/javatests/com/google/gerrit/server/restapi/change/CommentPorterTest.java b/javatests/com/google/gerrit/server/restapi/change/CommentPorterTest.java
index 46f9c5a..4c8750a 100644
--- a/javatests/com/google/gerrit/server/restapi/change/CommentPorterTest.java
+++ b/javatests/com/google/gerrit/server/restapi/change/CommentPorterTest.java
@@ -38,9 +38,10 @@
import com.google.gerrit.server.notedb.ChangeNotes;
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.restapi.change.CommentPorter.Metrics;
import com.google.gerrit.truth.NullAwareCorrespondence;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Arrays;
import java.util.Optional;
import org.eclipse.jgit.lib.ObjectId;
@@ -78,7 +79,10 @@
when(commentsUtil.determineCommitId(any(), any(), anyShort()))
.thenReturn(Optional.of(dummyObjectId));
when(diffOperations.listModifiedFiles(
- any(Project.NameKey.class), any(ObjectId.class), any(ObjectId.class)))
+ any(Project.NameKey.class),
+ any(ObjectId.class),
+ any(ObjectId.class),
+ any(DiffOptions.class)))
.thenThrow(DiffNotAvailableException.class);
ImmutableList<HumanComment> portedComments =
commentPorter.portComments(
@@ -101,7 +105,10 @@
when(commentsUtil.determineCommitId(any(), any(), anyShort()))
.thenReturn(Optional.of(dummyObjectId));
when(diffOperations.listModifiedFiles(
- any(Project.NameKey.class), any(ObjectId.class), any(ObjectId.class)))
+ any(Project.NameKey.class),
+ any(ObjectId.class),
+ any(ObjectId.class),
+ any(DiffOptions.class)))
.thenThrow(IllegalStateException.class);
ImmutableList<HumanComment> portedComments =
commentPorter.portComments(
@@ -144,7 +151,10 @@
when(commentsUtil.determineCommitId(any(), any(), anyShort()))
.thenReturn(Optional.of(dummyObjectId));
when(diffOperations.listModifiedFiles(
- any(Project.NameKey.class), any(ObjectId.class), any(ObjectId.class)))
+ any(Project.NameKey.class),
+ any(ObjectId.class),
+ any(ObjectId.class),
+ any(DiffOptions.class)))
.thenThrow(IllegalStateException.class);
ImmutableList<HumanComment> portedComments =
commentPorter.portComments(
@@ -173,7 +183,10 @@
.thenReturn(Optional.of(dummyObjectId));
// Throw an exception on the first diff request but return an actual value on the second.
when(diffOperations.listModifiedFiles(
- any(Project.NameKey.class), any(ObjectId.class), any(ObjectId.class)))
+ any(Project.NameKey.class),
+ any(ObjectId.class),
+ any(ObjectId.class),
+ any(DiffOptions.class)))
.thenThrow(IllegalStateException.class)
.thenReturn(ImmutableMap.of());
ImmutableList<HumanComment> portedComments =
@@ -200,7 +213,10 @@
when(commentsUtil.determineCommitId(any(), any(), anyShort()))
.thenReturn(Optional.of(dummyObjectId));
when(diffOperations.listModifiedFiles(
- any(Project.NameKey.class), any(ObjectId.class), any(ObjectId.class)))
+ any(Project.NameKey.class),
+ any(ObjectId.class),
+ any(ObjectId.class),
+ any(DiffOptions.class)))
.thenReturn(ImmutableMap.of());
ImmutableList<HumanComment> portedComments =
commentPorter.portComments(
@@ -215,7 +231,7 @@
changeId,
Account.id(123),
BranchNameKey.create(project, "myBranch"),
- new Timestamp(12345));
+ Instant.ofEpochMilli(12345));
}
private PatchSet createPatchset(PatchSet.Id id) {
@@ -223,7 +239,7 @@
.id(id)
.commitId(dummyObjectId)
.uploader(Account.id(123))
- .createdOn(new Timestamp(12345))
+ .createdOn(Instant.ofEpochMilli(12345))
.build();
}
@@ -246,7 +262,7 @@
return new HumanComment(
new Comment.Key(getUniqueUuid(), filePath, patchsetId.get()),
Account.id(100),
- new Timestamp(1234),
+ Instant.ofEpochMilli(1234),
(short) 1,
"Comment text",
"serverId",
diff --git a/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java b/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
index 44c3cef..2685a8b 100644
--- a/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
+++ b/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
@@ -23,6 +23,9 @@
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CommentsUtil;
import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@@ -124,9 +127,11 @@
/** Create a new change message with an id, message, timestamp and tag */
private static ChangeMessage newChangeMessage(String id, String message, String ts, String tag) {
ChangeMessage.Key key = ChangeMessage.key(Change.id(1), id);
- ChangeMessage cm =
- ChangeMessage.create(
- key, null, Timestamp.valueOf("2000-01-01 00:00:" + ts), null, message, null, tag);
+ Instant timestamp =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
+ .withZone(ZoneId.systemDefault())
+ .parse("2000-01-01 00:00:" + ts, Instant::from);
+ ChangeMessage cm = ChangeMessage.create(key, null, timestamp, null, message, null, tag);
return cm;
}
diff --git a/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java b/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java
index e5dd817..509447a 100644
--- a/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java
+++ b/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java
@@ -27,7 +27,6 @@
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Date;
import java.util.List;
import org.junit.Test;
@@ -84,7 +83,7 @@
return PatchSetApproval.builder()
.key(PatchSetApproval.key(PS_ID, accountId, labelId))
.value(value)
- .granted(Date.from(Instant.now()))
+ .granted(Instant.now())
.build();
}
diff --git a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
index 10599c6..1f22564 100644
--- a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
+++ b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
@@ -95,7 +95,7 @@
RevCommit masterCommit = repo.branch("master").commit().create();
RevCommit branchCommit = repo.branch("branch").commit().parent(masterCommit).create();
- try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
bu.addRepoOnlyOp(
new RepoOnlyOp() {
@Override
@@ -114,7 +114,7 @@
public void cannotExceedMaxUpdates() throws Exception {
Change.Id id = createChangeWithUpdates(MAX_UPDATES);
ObjectId oldMetaId = getMetaId(id);
- try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
bu.addOp(id, new AddMessageOp("Excessive update"));
ResourceConflictException thrown = assertThrows(ResourceConflictException.class, bu::execute);
assertThat(thrown)
@@ -130,7 +130,7 @@
Change.Id id = createChangeWithPatchSets(2);
ObjectId oldMetaId = getMetaId(id);
- try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
bu.addOp(id, new AddMessageOp("Update on PS1", PatchSet.id(id, 1)));
bu.addOp(id, new AddMessageOp("Update on PS2", PatchSet.id(id, 2)));
ResourceConflictException thrown = assertThrows(ResourceConflictException.class, bu::execute);
@@ -146,7 +146,7 @@
public void exceedingMaxUpdatesAllowedWithCompleteNoOp() throws Exception {
Change.Id id = createChangeWithUpdates(MAX_UPDATES);
ObjectId oldMetaId = getMetaId(id);
- try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
bu.addOp(
id,
new BatchUpdateOp() {
@@ -165,7 +165,7 @@
public void exceedingMaxUpdatesAllowedWithNoOpAfterPopulatingUpdate() throws Exception {
Change.Id id = createChangeWithUpdates(MAX_UPDATES);
ObjectId oldMetaId = getMetaId(id);
- try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
bu.addOp(
id,
new BatchUpdateOp() {
@@ -185,7 +185,7 @@
public void exceedingMaxUpdatesAllowedWithSubmit() throws Exception {
Change.Id id = createChangeWithUpdates(MAX_UPDATES);
ObjectId oldMetaId = getMetaId(id);
- try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
bu.addOp(id, new SubmitOp());
bu.execute();
}
@@ -197,7 +197,7 @@
public void exceedingMaxUpdatesAllowedWithSubmitAfterOtherOp() throws Exception {
Change.Id id = createChangeWithPatchSets(2);
ObjectId oldMetaId = getMetaId(id);
- try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
bu.addOp(id, new AddMessageOp("Message on PS1", PatchSet.id(id, 1)));
bu.addOp(id, new SubmitOp());
bu.execute();
@@ -212,7 +212,7 @@
public void exceedingMaxUpdatesAllowedWithAbandon() throws Exception {
Change.Id id = createChangeWithUpdates(MAX_UPDATES);
ObjectId oldMetaId = getMetaId(id);
- try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
bu.addOp(
id,
new BatchUpdateOp() {
@@ -235,7 +235,7 @@
Change.Id changeId = createChangeWithPatchSets(MAX_PATCH_SETS);
ObjectId oldMetaId = getMetaId(changeId);
ChangeNotes notes = changeNotesFactory.create(project, changeId);
- try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
ObjectId commitId =
repo.amend(notes.getCurrentPatchSet().commitId()).message("kaboom").create();
bu.addOp(
@@ -257,7 +257,7 @@
Change.Id changeId = createChangeWithUpdates(1);
ChangeNotes notes = changeNotesFactory.create(project, changeId);
- try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
ObjectId commitId =
repo.amend(notes.getCurrentPatchSet().commitId())
.add("bar.txt", "bar")
@@ -285,7 +285,7 @@
int cacheSizeBefore = diffSummaryCache.asMap().size();
// We don't want to depend on the test helper used above so we perform an explicit commit here.
- try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
ObjectId commitId =
repo.amend(notes.getCurrentPatchSet().commitId())
.add("bar.txt", "bar")
@@ -309,7 +309,7 @@
checkArgument(totalUpdates > 0);
checkArgument(totalUpdates <= MAX_UPDATES);
Change.Id id = Change.id(sequences.nextChangeId());
- try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
bu.insertChange(
changeInserterFactory.create(
id, repo.commit().message("Change").insertChangeId().create(), "refs/heads/master"));
@@ -317,7 +317,7 @@
}
assertThat(getUpdateCount(id)).isEqualTo(1);
for (int i = 2; i <= totalUpdates; i++) {
- try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
bu.addOp(id, new AddMessageOp("Update " + i));
bu.execute();
}
@@ -331,7 +331,7 @@
Change.Id id = createChangeWithUpdates(MAX_UPDATES - 2);
ChangeNotes notes = changeNotesFactory.create(project, id);
for (int i = 2; i <= patchSets; ++i) {
- try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+ try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
ObjectId commitId =
repo.amend(notes.getCurrentPatchSet().commitId()).message("PS" + i).create();
bu.addOp(
diff --git a/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java b/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
index 0bb4de4..ebdf2d9 100644
--- a/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
+++ b/javatests/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
@@ -269,8 +269,8 @@
.filter(s -> !s.isEmpty())
.map(
(String cookieValue) -> {
- String[] kv = cookieValue.split("=");
- return new Cookie(kv[0], kv[1]);
+ List<String> kv = Splitter.on("=").splitToList(cookieValue);
+ return new Cookie(kv.get(0), kv.get(1));
})
.collect(toList())
.toArray(new Cookie[0]);
diff --git a/lib/BUILD b/lib/BUILD
index f924e4ca..ce83ba1 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -47,13 +47,6 @@
)
java_library(
- name = "jgit-ssh-jsch",
- data = ["//lib:LICENSE-jgit"],
- visibility = ["//visibility:public"],
- exports = ["@jgit//org.eclipse.jgit.ssh.jsch:ssh-jsch"],
-)
-
-java_library(
name = "jgit-ssh-apache",
data = ["//lib:LICENSE-jgit"],
visibility = ["//visibility:public"],
@@ -128,6 +121,15 @@
)
java_library(
+ name = "guava-testlib",
+ data = ["//lib:LICENSE-Apache2.0"],
+ visibility = ["//visibility:public"],
+ exports = [
+ "@guava-testlib//jar",
+ ],
+)
+
+java_library(
name = "caffeine",
data = ["//lib:LICENSE-Apache2.0"],
visibility = [
@@ -153,13 +155,6 @@
)
java_library(
- name = "jsch",
- data = ["//lib:LICENSE-jsch"],
- visibility = ["//visibility:public"],
- exports = ["@jsch//jar"],
-)
-
-java_library(
name = "juniversalchardet",
data = ["//lib:LICENSE-MPL1.1"],
visibility = ["//visibility:public"],
@@ -540,18 +535,6 @@
exports = ["@icu4j//jar"],
)
-java_library(
- name = "javax-annotation",
- data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
- visibility = [
- "//java/com/google/gerrit/acceptance:__pkg__",
- "//java/com/google/gerrit/extensions:__pkg__",
- "//java/com/google/gerrit/server:__pkg__",
- "//plugins:__subpackages__",
- ],
- exports = ["@javax-annotation//jar"],
-)
-
sh_test(
name = "nongoogle_test",
srcs = ["nongoogle_test.sh"],
diff --git a/lib/log/BUILD b/lib/log/BUILD
index 21c4d47..6a85bd1 100644
--- a/lib/log/BUILD
+++ b/lib/log/BUILD
@@ -18,14 +18,6 @@
)
java_library(
- name = "impl-log4j",
- data = ["//lib:LICENSE-slf4j"],
- visibility = ["//visibility:public"],
- exports = ["@impl-log4j//jar"],
- runtime_deps = [":log4j"],
-)
-
-java_library(
name = "jcl-over-slf4j",
data = ["//lib:LICENSE-slf4j"],
visibility = ["//visibility:public"],
diff --git a/lib/nongoogle_test.sh b/lib/nongoogle_test.sh
index 06dbd08..957c33d 100755
--- a/lib/nongoogle_test.sh
+++ b/lib/nongoogle_test.sh
@@ -16,10 +16,12 @@
commons-io
dropwizard-core
eddsa
+error-prone-annotations
flogger
flogger-log4j-backend
flogger-system-backend
guava
+guava-testlib
guice-assistedinject
guice-library
guice-servlet
diff --git a/modules/jgit b/modules/jgit
index 60b81c5..4d34cdf 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit 60b81c5a9280e44fa48d533a61f915382b2b9ce2
+Subproject commit 4d34cdf3459022d0878dfbd099c6f7b7ea03ea73
diff --git a/package.json b/package.json
index a492055..cb2aa53 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"eslint-plugin-regex": "^1.8.0",
"gts": "^3.1.0",
"lit-analyzer": "^1.2.1",
+ "npm-run-all": "^4.1.5",
"prettier": "2.3.1",
"rollup": "^2.45.2",
"terser": "^5.6.1",
@@ -39,8 +40,10 @@
"litlint": "npm run safe_bazelisk run polygerrit-ui/app:lit_analysis",
"test:debug": "npm run compile:local && npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --root '.ts-out/polygerrit-ui/app/' --browsers ChromeDev --no-single-run --test-files",
"test:single": "npm run compile:local && npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --root '.ts-out/polygerrit-ui/app/' --test-files",
+ "test:watch": "npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --root '.ts-out/polygerrit-ui/app/' --auto-watch --no-single-run --test-files",
"polylint": "npm run safe_bazelisk test //polygerrit-ui/app:polylint_test",
- "polylint:dev": "rm -rf ./polygerrit-ui/app/tmpl_out && npm run safe_bazelisk build //polygerrit-ui/app:template_test_tar && mkdir ./polygerrit-ui/app/tmpl_out && tar -xf bazel-bin/polygerrit-ui/app/template_test_tar.tar -C ./polygerrit-ui/app/tmpl_out"
+ "polylint:dev": "rm -rf ./polygerrit-ui/app/tmpl_out && npm run safe_bazelisk build //polygerrit-ui/app:template_test_tar && mkdir ./polygerrit-ui/app/tmpl_out && tar -xf bazel-bin/polygerrit-ui/app/template_test_tar.tar -C ./polygerrit-ui/app/tmpl_out",
+ "watch": "npm run compile:local && run-p -r compile:watch \"test:watch -- {*}\" --"
},
"repository": {
"type": "git",
diff --git a/plugins/BUILD b/plugins/BUILD
index 8eed4e8..1271f04 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -84,7 +84,6 @@
"//lib/httpcomponents:httpcore",
"//lib:jgit-servlet",
"//lib:jgit",
- "//lib:jgit-ssh-jsch",
"//lib:jsr305",
"//lib/log:api",
"//lib/log:log4j",
@@ -99,7 +98,6 @@
"//lib:guava-retrying",
"//lib:gson",
"//lib:icu4j",
- "//lib:jsch",
"//lib:mime-util",
"//lib:protobuf",
"//lib:servlet-api-without-neverlink",
diff --git a/plugins/delete-project b/plugins/delete-project
index 612f143..5717bad 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit 612f143792652d571ecfcb19915ad5754a3ba1a7
+Subproject commit 5717badf4250dfe900c05fc00d0758a09ba77297
diff --git a/plugins/download-commands b/plugins/download-commands
index d2f0cae..1a30359 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit d2f0cae51a269ca660f221172cf010e2d528b661
+Subproject commit 1a303596d8aa08cf3135819eaf8451d1a2e004b6
diff --git a/plugins/gitiles b/plugins/gitiles
index fa993c0..97ce60f 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit fa993c05a1861c766c79cc304cc2ce4e9ae2905a
+Subproject commit 97ce60f8bb4dbf40dde79cf56db6425c384dabcf
diff --git a/plugins/hooks b/plugins/hooks
index 4e07d16..d760a4b 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit 4e07d16a644ea823f6538a176621acee466d865b
+Subproject commit d760a4b49f79c31696e425e761cad0fcbfe410c9
diff --git a/plugins/plugin-manager b/plugins/plugin-manager
index e0664f6..dbd6820 160000
--- a/plugins/plugin-manager
+++ b/plugins/plugin-manager
@@ -1 +1 @@
-Subproject commit e0664f668ab5bac96a1e105b80d886de66743b1b
+Subproject commit dbd68200d867513e2c0449798476e275aaf08cfd
diff --git a/plugins/replication b/plugins/replication
index 4f5de60..98926b4 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 4f5de60081d7c098d09c842e3ea9d5bc920df71b
+Subproject commit 98926b44a199b5a7049232f6c3b3758267368f8f
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index a28ae59..6226d01 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit a28ae590486934690e4e0a95d7eb75f8b60644a6
+Subproject commit 6226d01c563846ae479cb4fdafd698b31472772c
diff --git a/plugins/webhooks b/plugins/webhooks
index 8df6303..d8815bf 160000
--- a/plugins/webhooks
+++ b/plugins/webhooks
@@ -1 +1 @@
-Subproject commit 8df6303e652857c3d7cee3348d2cb7d40a7db1af
+Subproject commit d8815bf9660b6655696db242b8ad2801e866c036
diff --git a/polygerrit-ui/FE_Style_Guide.md b/polygerrit-ui/FE_Style_Guide.md
index a636119..6673cdf 100644
--- a/polygerrit-ui/FE_Style_Guide.md
+++ b/polygerrit-ui/FE_Style_Guide.md
@@ -103,29 +103,6 @@
this._count++;
}
}
-
-// app-context.js
-export const appContext = {
- //...
- mouseClickCounterService: null,
- keypressCounterService: null,
-};
-
-// services/app-context-init.js
-export function initAppContext() {
- //...
- // Add the following line before the Object.defineProperties(appContext, registeredServices);
- addService('mouseClickCounterService', () => new CounterService());
- addService('keypressCounterService', () => new CounterService());
- // If a service depends on other services, pass dependencies as shown below
- // If circular dependencies exist, app-init-context tests fail with timeout or stack overflow
- // (we are going to improve it in the future)
- addService('analyticService', () =>
- new CounterService(appContext.mouseClickCounterService, appContext.keypressCounterService));
- //...
- // This following line must remains the last one in the initAppContext
- Object.defineProperties(appContext, registeredServices);
-}
```
**Bad:**
@@ -146,7 +123,7 @@
If a class/service depends on some other service (or multiple services), the class must accept all dependencies
as parameters in the constructor.
-Do not use appContext anywhere else in a class.
+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
@@ -166,19 +143,19 @@
**Bad:**
```Javascript
-import {appContext} from "./app-context";
+import {getAppContext} from "./app-context";
export class UserService {
constructor() {
// Incorrect: you must pass all dependencies to a constructor
- this._restApiService = appContext.restApiService;
+ this._restApiService = getAppContext().restApiService;
}
}
export class AdminService {
isAdmin() {
// Incorrect: you must pass all dependencies to a constructor
- return appContext.restApiService.sendRequest(...);
+ return getAppContext().restApiService.sendRequest(...);
}
}
@@ -210,11 +187,11 @@
export class MyCustomElement extends ...{
constructor() {
super(); //This is mandatory to call parent constructor
- this._userService = appContext.userService;
+ this._userModel = appContext.userModel;
}
//...
_getUserName() {
- return this._userService.activeUserName();
+ return this._userModel.activeUserName();
}
}
```
@@ -226,12 +203,12 @@
export class MyCustomElement extends ...{
created() {
// Incorrect: assign all dependencies in the constructor
- this._userService = appContext.userService;
+ this._userModel = appContext.userModel;
}
//...
_getUserName() {
// Incorrect: use appContext outside of a constructor
- return appContext.userService.activeUserName();
+ return appContext.userModel.activeUserName();
}
}
```
@@ -260,7 +237,7 @@
constructor() {
super();
// Assign services here
- this._userService = appContext.userService;
+ this._userModel = appContext.userModel;
// Code from the created method - put it before existing actions in constructor
createdAction1();
createdAction2();
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 3a647d4..14f1e95 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -15,6 +15,7 @@
"embed",
"gr-diff",
"mixins",
+ "models",
"samples",
"scripts",
"services",
@@ -94,19 +95,9 @@
# so template tests pass.
# TODO: fix problems reported by template checker in these files.
ignore_templates_list = [
- "elements/admin/gr-admin-view/gr-admin-view_html.ts",
- "elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts",
- "elements/admin/gr-group-members/gr-group-members_html.ts",
- "elements/admin/gr-group/gr-group_html.ts",
"elements/admin/gr-permission/gr-permission_html.ts",
- "elements/admin/gr-plugin-list/gr-plugin-list_html.ts",
"elements/admin/gr-repo-access/gr-repo-access_html.ts",
- "elements/admin/gr-repo-commands/gr-repo-commands_html.ts",
- "elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.ts",
- "elements/admin/gr-repo/gr-repo_html.ts",
"elements/admin/gr-rule-editor/gr-rule-editor_html.ts",
- "elements/change-list/gr-change-list-view/gr-change-list-view_html.ts",
- "elements/change-list/gr-change-list/gr-change-list_html.ts",
"elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts",
"elements/change/gr-change-actions/gr-change-actions_html.ts",
"elements/change/gr-change-metadata/gr-change-metadata_html.ts",
@@ -125,10 +116,10 @@
"elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.ts",
"elements/diff/gr-diff-view/gr-diff-view_html.ts",
"elements/diff/gr-diff/gr-diff_html.ts",
- "elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts",
"elements/gr-app-element_html.ts",
"elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.ts",
"elements/shared/gr-account-list/gr-account-list_html.ts",
+ "models/dependency.ts",
]
sources_for_template_checking = glob(
@@ -277,17 +268,22 @@
],
)
-nodejs_binary(
+nodejs_test(
name = "lit_analysis",
data = [
":lit_analysis_src_code",
"@npm//lit-analyzer",
],
entry_point = "@npm//:node_modules/lit-analyzer/cli.js",
+ tags = [
+ "local",
+ "manual",
+ ],
templated_args = [
"**/elements/**/*.ts",
"--strict",
"--rules.no-property-visibility-mismatch off",
"--rules.no-incompatible-property-type off",
+ "--rules.no-incompatible-type-binding off",
],
)
diff --git a/polygerrit-ui/app/api/change-actions.ts b/polygerrit-ui/app/api/change-actions.ts
index 4380195..e3143b8 100644
--- a/polygerrit-ui/app/api/change-actions.ts
+++ b/polygerrit-ui/app/api/change-actions.ts
@@ -42,7 +42,6 @@
DELETE_EDIT = 'deleteEdit',
EDIT = 'edit',
FOLLOW_UP = 'followup',
- IGNORE = 'ignore',
MOVE = 'move',
PRIVATE = 'private',
PRIVATE_DELETE = 'private.delete',
@@ -56,7 +55,6 @@
REVIEWED = 'reviewed',
STOP_EDIT = 'stopEdit',
SUBMIT = 'submit',
- UNIGNORE = 'unignore',
UNREVIEWED = 'unreviewed',
WIP = 'wip',
INCLUDED_IN = 'includedIn',
diff --git a/polygerrit-ui/app/api/change-reply.ts b/polygerrit-ui/app/api/change-reply.ts
index 0b00b10..3d652db 100644
--- a/polygerrit-ui/app/api/change-reply.ts
+++ b/polygerrit-ui/app/api/change-reply.ts
@@ -30,7 +30,7 @@
) => void;
export declare interface ChangeReplyPluginApi {
- getLabelValue(label: string): string;
+ getLabelValue(label: string): string | number | undefined;
setLabelValue(label: string, value: string): void;
diff --git a/polygerrit-ui/app/api/checks.ts b/polygerrit-ui/app/api/checks.ts
index d52a555..d1c320f 100644
--- a/polygerrit-ui/app/api/checks.ts
+++ b/polygerrit-ui/app/api/checks.ts
@@ -186,7 +186,11 @@
* RUNNABLE: Not run (yet). Mostly useful for runs that the user can trigger
* (see actions) and for indicating that a check was not run at a
* later attempt. Cannot contain results.
- * RUNNING: Subsumes "scheduled".
+ * RUNNING: The run is in progress.
+ * SCHEDULED: Refinement of RUNNING: The run was triggered, but is not yet
+ * running. It may have to wait for resources or for some other run
+ * to finish. The UI treats this mostly identical to RUNNING, but
+ * uses a differnt icon.
* COMPLETED: The attempt of the run has finished. Does not indicate at all
* whether the run was successful or not. Outcomes can and should
* be modeled using the CheckResult entity.
@@ -221,7 +225,7 @@
* each plugin. The most important actions (which get special UI treatment)
* are:
* "Run" for RUNNABLE and COMPLETED runs.
- * "Cancel" for RUNNING runs.
+ * "Cancel" for RUNNING and SCHEDULED runs.
*/
actions?: Action[];
@@ -316,9 +320,11 @@
errorMessage?: string;
}
+/** See CheckRun.status for documentation. */
export enum RunStatus {
RUNNABLE = 'RUNNABLE',
RUNNING = 'RUNNING',
+ SCHEDULED = 'SCHEDULED',
COMPLETED = 'COMPLETED',
}
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index 905d6be..c692775 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -50,6 +50,8 @@
content: DiffContent[];
/** Whether the file is binary. */
binary?: boolean;
+ /** A list of strings representing the patch set diff header. */
+ diff_header?: string[];
}
/**
@@ -284,7 +286,9 @@
}
export declare interface LineRange {
+ /** 1-based, inclusive. */
start_line: number;
+ /** 1-based, inclusive. */
end_line: number;
}
@@ -323,6 +327,11 @@
lineNum: LineNumber;
}
+export declare interface DisplayLine {
+ side: Side;
+ lineNum: LineNumber;
+}
+
/** All types of button for expanding diff sections */
export enum ContextButtonType {
ABOVE = 'above',
@@ -446,6 +455,14 @@
/** An instance of the GrDiff Webcomponent */
export declare interface GrDiff extends HTMLElement {
/**
+ * A line that should not be collapsed, e.g. because it contains a
+ * search result, or is pointed to from the URL.
+ * This is considered during rendering, but changing this does not
+ * automatically trigger a re-render.
+ */
+ lineOfInterest?: DisplayLine;
+
+ /**
* Return line number element for reading only,
*
* This is useful e.g. to determine where on screen certain lines are,
@@ -484,5 +501,20 @@
createCommentInPlace(): void;
resetScrollMode(): void;
- moveToLineNumber(lineNum: number, side: Side, path?: string): void;
+
+ /**
+ * Moves to a specific line number in the diff
+ *
+ * @param lineNum which line number should be selected
+ * @param side which side should be selected
+ * @param path file path for the file that should be selected
+ * @param intentionalMove Defines if move-related controls should be applied
+ * (e.g. GrCursorManager.focusOnMove)
+ **/
+ moveToLineNumber(
+ lineNum: number,
+ side: Side,
+ path?: string,
+ intentionalMove?: boolean
+ ): void;
}
diff --git a/polygerrit-ui/app/api/package.json b/polygerrit-ui/app/api/package.json
index 8af6832..ac1e0c8 100644
--- a/polygerrit-ui/app/api/package.json
+++ b/polygerrit-ui/app/api/package.json
@@ -1,6 +1,6 @@
{
"name": "@gerritcodereview/typescript-api",
- "version": "3.4.4",
+ "version": "3.4.5",
"description": "Gerrit Code Review - TypeScript API",
"homepage": "https://www.gerritcodereview.com/",
"browser": true,
diff --git a/polygerrit-ui/app/api/plugin.ts b/polygerrit-ui/app/api/plugin.ts
index 7a56ff7..418cc67 100644
--- a/polygerrit-ui/app/api/plugin.ts
+++ b/polygerrit-ui/app/api/plugin.ts
@@ -49,7 +49,22 @@
}
export declare interface PluginApi {
+ /**
+ * The raw URL of the plugin's js bundle, e.g.:
+ * https://cdn.googlesource.com/polygerrit_assets/533.0/plugins/codemirror_editor/static/codemirror_editor.js'
+ */
_url?: URL;
+ /**
+ * The base path of plugin related resources. Depends on whether the plugin
+ * was loaded from the same origin as the Gerrit web app itself.
+ *
+ * Same origin: The base path of all Gerrit URLs, e.g.:
+ * https://gerrit-review.googlesource.com/
+ *
+ * Different origin: The root path of plugin files, e.g.:
+ * https://cdn.googlesource.com/polygerrit_assets/533.0/plugins/codemirror_editor/'
+ */
+ url(): string;
admin(): AdminPluginApi;
annotationApi(): AnnotationPluginApi;
attributeHelper(element: Element): AttributeHelperPluginApi;
diff --git a/polygerrit-ui/app/api/rest-api.ts b/polygerrit-ui/app/api/rest-api.ts
index 39b40b6..ca3f2d7 100644
--- a/polygerrit-ui/app/api/rest-api.ts
+++ b/polygerrit-ui/app/api/rest-api.ts
@@ -286,7 +286,6 @@
submit?: ActionInfo;
topic?: ActionInfo;
hashtags?: ActionInfo;
- assignee?: ActionInfo;
ready?: ActionInfo;
includedIn?: ActionInfo;
}
@@ -363,7 +362,6 @@
submit_whole_topic?: boolean;
disable_private_changes?: boolean;
mergeability_computation_behavior: MergeabilityComputationBehavior;
- enable_assignee: boolean;
}
export type ChangeId = BrandType<string, '_changeId'>;
@@ -378,7 +376,6 @@
branch: BranchName;
topic?: TopicName;
attention_set?: IdToAttentionSetMap;
- assignee?: AccountInfo;
hashtags?: Hashtag[];
change_id: ChangeId;
subject: string;
@@ -389,7 +386,6 @@
submitter?: AccountInfo;
starred?: boolean; // not set if false
stars?: StarLabel[];
- reviewed?: boolean; // not set if false
submit_type?: SubmitType;
mergeable?: boolean;
submittable?: boolean;
@@ -424,6 +420,7 @@
contains_git_conflicts?: boolean;
internalHost?: string; // TODO(TS): provide an explanation what is its
submit_requirements?: SubmitRequirementResultInfo[];
+ submit_records?: SubmitRecordInfo[];
}
// The ID of the change in the format "'<project>~<branch>~<Change-Id>'"
@@ -524,6 +521,7 @@
plugin_config?: PluginNameToPluginParametersMap;
actions?: {[viewName: string]: ActionInfo};
reject_empty_commit?: InheritedBooleanInfo;
+ enable_reviewer_by_email: InheritedBooleanInfo;
}
export declare interface ConfigListParameterInfo
@@ -951,7 +949,6 @@
fetch?: {[protocol: string]: FetchInfo};
commit?: CommitInfo;
files?: {[filename: string]: FileInfo};
- actions?: ActionNameToActionInfoMap;
reviewed?: boolean;
commit_with_footers?: boolean;
push_certificate?: PushCertificateInfo;
@@ -1065,7 +1062,7 @@
url: string;
/** URL to the icon of the link. */
image_url?: string;
- /* The links target. */
+ /* Value of the "target" attribute for anchor elements. */
target?: string;
}
@@ -1105,6 +1102,64 @@
UNSATISFIED = 'UNSATISFIED',
OVERRIDDEN = 'OVERRIDDEN',
NOT_APPLICABLE = 'NOT_APPLICABLE',
+ ERROR = 'ERROR',
+ FORCED = 'FORCED',
}
export type UrlEncodedRepoName = BrandType<string, '_urlEncodedRepoName'>;
+
+/**
+ * The SubmitRecordInfo entity describes results from a submit_rule.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#submit-record-info
+ */
+export declare interface SubmitRecordInfo {
+ rule_name: string;
+ status?: SubmitRecordInfoStatus;
+ labels?: SubmitRecordInfoLabel[];
+ requirements?: Requirement[];
+ error_message?: string;
+}
+
+export enum SubmitRecordInfoStatus {
+ OK = 'OK',
+ NOT_READY = 'NOT_READY',
+ CLOSED = 'CLOSED',
+ FORCED = 'FORCED',
+ RULE_ERROR = 'RULE_ERROR',
+}
+
+export enum LabelStatus {
+ /**
+ * This label provides what is necessary for submission.
+ */
+ OK = 'OK',
+ /**
+ * This label prevents the change from being submitted.
+ */
+ REJECT = 'REJECT',
+ /**
+ * The label may be set, but it's neither necessary for submission
+ * nor does it block submission if set.
+ */
+ MAY = 'MAY',
+ /**
+ * The label is required for submission, but has not been satisfied.
+ */
+ NEED = 'NEED',
+ /**
+ * 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.
+ */
+ IMPOSSIBLE = 'IMPOSSIBLE',
+ OPTIONAL = 'OPTIONAL',
+}
+
+/**
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#submit-record-info
+ */
+export declare interface SubmitRecordInfoLabel {
+ label: string;
+ status: LabelStatus;
+ appliedBy: AccountInfo;
+}
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
index 645e770..3024597 100644
--- a/polygerrit-ui/app/constants/constants.ts
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -92,8 +92,6 @@
TAG_UNSET_PRIVATE = 'autogenerated:gerrit:unsetPrivate',
TAG_SET_READY = 'autogenerated:gerrit:setReadyForReview',
TAG_SET_WIP = 'autogenerated:gerrit:setWorkInProgress',
- TAG_SET_ASSIGNEE = 'autogenerated:gerrit:setAssignee',
- TAG_UNSET_ASSIGNEE = 'autogenerated:gerrit:deleteAssignee',
TAG_MERGED = 'autogenerated:gerrit:merged',
TAG_REVERT = 'autogenerated:gerrit:revert',
}
@@ -256,7 +254,6 @@
export function createDefaultPreferences() {
return {
changes_per_page: 25,
- default_diff_view: DiffViewMode.SIDE_BY_SIDE,
diff_view: DiffViewMode.SIDE_BY_SIDE,
size_bar_in_change_table: true,
} as PreferencesInfo;
diff --git a/polygerrit-ui/app/constants/reporting.ts b/polygerrit-ui/app/constants/reporting.ts
index a10bdda..78fffe3 100644
--- a/polygerrit-ui/app/constants/reporting.ts
+++ b/polygerrit-ui/app/constants/reporting.ts
@@ -92,10 +92,20 @@
FILE_EXPAND_ALL = 'ExpandAllDiffs',
// This measures the same interval as ExpandAllDiffs, but the result is divided by the number of diffs expanded.
FILE_EXPAND_ALL_AVG = 'ExpandAllPerDiff',
+ // Time for making the REST API call of creating a draft comment.
+ DRAFT_CREATE = 'CreateDraftComment',
+ // Time for making the REST API call of update a draft comment.
+ DRAFT_UPDATE = 'UpdateDraftComment',
+ // Time for making the REST API call of deleting a draft comment.
+ DRAFT_DISCARD = 'DiscardDraftComment',
}
export enum Interaction {
TOGGLE_SHOW_ALL_BUTTON = 'toggle show all button',
SHOW_TAB = 'show-tab',
ATTENTION_SET_CHIP = 'attention-set-chip',
+ SAVE_COMMENT = 'save-comment',
+ COMMENT_SAVED = 'comment-saved',
+ DISCARD_COMMENT = 'discard-comment',
+ COMMENT_DISCARDED = 'comment-discarded',
}
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 ea70d7e..9330f08 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
@@ -15,23 +15,23 @@
* limitations under the License.
*/
-import '../../../styles/gr-table-styles';
-import '../../../styles/shared-styles';
import '../../shared/gr-dialog/gr-dialog';
import '../../shared/gr-list-view/gr-list-view';
import '../../shared/gr-overlay/gr-overlay';
import '../gr-create-group-dialog/gr-create-group-dialog';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-admin-group-list_html';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {customElement, property, observe, computed} from '@polymer/decorators';
import {AppElementAdminParams} from '../../gr-app-types';
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';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
+import {tableStyles} from '../../../styles/gr-table-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, html} from 'lit';
+import {customElement, query, property, state} from 'lit/decorators';
+import {assertIsDefined} from '../../../utils/common-util';
declare global {
interface HTMLElementTagNameMap {
@@ -39,18 +39,13 @@
}
}
-export interface GrAdminGroupList {
- $: {
- createOverlay: GrOverlay;
- createNewModal: GrCreateGroupDialog;
- };
-}
-
@customElement('gr-admin-group-list')
-export class GrAdminGroupList extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
+export class GrAdminGroupList extends LitElement {
+ readonly path = '/admin/groups';
+
+ @query('#createOverlay') private createOverlay?: GrOverlay;
+
+ @query('#createNewModal') private createNewModal?: GrCreateGroupDialog;
@property({type: Object})
params?: AppElementAdminParams;
@@ -58,131 +53,200 @@
/**
* Offset of currently visible query results.
*/
- @property({type: Number})
- _offset = 0;
+ @state() private offset = 0;
- @property({type: String})
- readonly _path = '/admin/groups';
+ @state() private hasNewGroupName = false;
- @property({type: Boolean})
- _hasNewGroupName = false;
+ @state() private createNewCapability = false;
- @property({type: Boolean})
- _createNewCapability = false;
+ // private but used in test
+ @state() groups: GroupInfo[] = [];
- @property({type: Array})
- _groups: GroupInfo[] = [];
+ @state() private groupsPerPage = 25;
- /**
- * Because we request one more than the groupsPerPage, _shownGroups
- * may be one less than _groups.
- * */
- @computed('_groups')
- get _shownGroups() {
- return this._groups.slice(0, SHOWN_ITEMS_COUNT);
- }
+ // private but used in test
+ @state() loading = true;
- @property({type: Number})
- _groupsPerPage = 25;
+ @state() private filter = '';
- @property({type: Boolean})
- _loading = true;
-
- @property({type: String})
- _filter = '';
-
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
override connectedCallback() {
super.connectedCallback();
- this._getCreateGroupCapability();
+ this.getCreateGroupCapability();
fireTitleChange(this, 'Groups');
- this._maybeOpenCreateOverlay(this.params);
}
- @observe('params')
- _paramsChanged(params: AppElementAdminParams) {
- this._loading = true;
- this._filter = params?.filter ?? '';
- this._offset = Number(params?.offset ?? 0);
+ static override get styles() {
+ return [tableStyles, sharedStyles];
+ }
- return this._getGroups(this._filter, this._groupsPerPage, this._offset);
+ override render() {
+ return html`
+ <gr-list-view
+ .createNew=${this.createNewCapability}
+ .filter=${this.filter}
+ .items=${this.groups}
+ .itemsPerPage=${this.groupsPerPage}
+ .loading=${this.loading}
+ .offset=${this.offset}
+ .path=${this.path}
+ @create-clicked=${() => this.handleCreateClicked()}
+ >
+ <table id="list" class="genericList">
+ <tbody>
+ <tr class="headerRow">
+ <th class="name topHeader">Group Name</th>
+ <th class="description topHeader">Group Description</th>
+ <th class="visibleToAll topHeader">Visible To All</th>
+ </tr>
+ <tr
+ id="loading"
+ class="loadingMsg ${this.loading ? 'loading' : ''}"
+ >
+ <td>Loading...</td>
+ </tr>
+ </tbody>
+ <tbody class=${this.loading ? 'loading' : ''}>
+ ${this.groups
+ .slice(0, SHOWN_ITEMS_COUNT)
+ .map(group => this.renderGroupList(group))}
+ </tbody>
+ </table>
+ </gr-list-view>
+ <gr-overlay id="createOverlay" with-backdrop>
+ <gr-dialog
+ id="createDialog"
+ class="confirmDialog"
+ ?disabled=${!this.hasNewGroupName}
+ confirm-label="Create"
+ confirm-on-enter
+ @confirm=${() => this.handleCreateGroup()}
+ @cancel=${() => this.handleCloseCreate()}
+ >
+ <div class="header" slot="header">Create Group</div>
+ <div class="main" slot="main">
+ <gr-create-group-dialog
+ id="createNewModal"
+ @has-new-group-name=${this.handleHasNewGroupName}
+ ></gr-create-group-dialog>
+ </div>
+ </gr-dialog>
+ </gr-overlay>
+ `;
+ }
+
+ private renderGroupList(group: GroupInfo) {
+ return html`
+ <tr class="table">
+ <td class="name">
+ <a href=${this.computeGroupUrl(group.id)}>${group.name}</a>
+ </td>
+ <td class="description">${group.description}</td>
+ <td class="visibleToAll">
+ ${group.options?.visible_to_all === true ? 'Y' : 'N'}
+ </td>
+ </tr>
+ `;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('params')) {
+ this.paramsChanged();
+ }
+ }
+
+ // private but used in test
+ paramsChanged() {
+ this.filter = this.params?.filter ?? '';
+ this.offset = Number(this.params?.offset ?? 0);
+ this.maybeOpenCreateOverlay(this.params);
+
+ return this.getGroups(this.filter, this.groupsPerPage, this.offset);
}
/**
* Opens the create overlay if the route has a hash 'create'
+ *
+ * private but used in test
*/
- _maybeOpenCreateOverlay(params?: AppElementAdminParams) {
+ maybeOpenCreateOverlay(params?: AppElementAdminParams) {
if (params?.openCreateModal) {
- this.$.createOverlay.open();
+ assertIsDefined(this.createOverlay, 'createOverlay');
+ this.createOverlay.open();
}
}
/**
* Generates groups link (/admin/groups/<uuid>)
+ *
+ * private but used in test
*/
- _computeGroupUrl(id: string) {
+ computeGroupUrl(id: string) {
return GerritNav.getUrlForGroup(decodeURIComponent(id) as GroupId);
}
- _getCreateGroupCapability() {
+ private getCreateGroupCapability() {
return this.restApiService.getAccount().then(account => {
- if (!account) {
- return;
- }
+ if (!account) return;
return this.restApiService
.getAccountCapabilities(['createGroup'])
.then(capabilities => {
if (capabilities?.createGroup) {
- this._createNewCapability = true;
+ this.createNewCapability = true;
}
});
});
}
- _getGroups(filter: string, groupsPerPage: number, offset?: number) {
- this._groups = [];
+ private getGroups(filter: string, groupsPerPage: number, offset?: number) {
+ this.groups = [];
+ this.loading = true;
return this.restApiService
.getGroups(filter, groupsPerPage, offset)
.then(groups => {
- if (!groups) {
- return;
- }
- this._groups = Object.keys(groups).map(key => {
+ if (!groups) return;
+ this.groups = Object.keys(groups).map(key => {
const group = groups[key];
group.name = key as GroupName;
return group;
});
- this._loading = false;
+ })
+ .finally(() => {
+ this.loading = false;
});
}
- _refreshGroupsList() {
+ private refreshGroupsList() {
this.restApiService.invalidateGroupsCache();
- return this._getGroups(this._filter, this._groupsPerPage, this._offset);
+ return this.getGroups(this.filter, this.groupsPerPage, this.offset);
}
- _handleCreateGroup() {
- this.$.createNewModal.handleCreateGroup().then(() => {
- this._refreshGroupsList();
+ // private but used in test
+ handleCreateGroup() {
+ assertIsDefined(this.createNewModal, 'createNewModal');
+ this.createNewModal.handleCreateGroup().then(() => {
+ this.refreshGroupsList();
});
}
- _handleCloseCreate() {
- this.$.createOverlay.close();
+ // private but used in test
+ handleCloseCreate() {
+ assertIsDefined(this.createOverlay, 'createOverlay');
+ this.createOverlay.close();
}
- _handleCreateClicked() {
- this.$.createOverlay.open().then(() => {
- this.$.createNewModal.focus();
+ // private but used in test
+ handleCreateClicked() {
+ assertIsDefined(this.createOverlay, 'createOverlay');
+ this.createOverlay.open().then(() => {
+ assertIsDefined(this.createNewModal, 'createNewModal');
+ this.createNewModal.focus();
});
}
- _visibleToAll(item: GroupInfo) {
- return item.options?.visible_to_all === true ? 'Y' : 'N';
- }
-
- computeLoadingClass(loading: boolean) {
- return loading ? 'loading' : '';
+ private handleHasNewGroupName() {
+ assertIsDefined(this.createNewModal, 'createNewModal');
+ this.hasNewGroupName = !!this.createNewModal.name;
}
}
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.ts b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.ts
deleted file mode 100644
index 91863a9..0000000
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-table-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <gr-list-view
- create-new="[[_createNewCapability]]"
- filter="[[_filter]]"
- items="[[_groups]]"
- items-per-page="[[_groupsPerPage]]"
- loading="[[_loading]]"
- offset="[[_offset]]"
- on-create-clicked="_handleCreateClicked"
- path="[[_path]]"
- >
- <table id="list" class="genericList">
- <tbody>
- <tr class="headerRow">
- <th class="name topHeader">Group Name</th>
- <th class="description topHeader">Group Description</th>
- <th class="visibleToAll topHeader">Visible To All</th>
- </tr>
- <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
- <td>Loading...</td>
- </tr>
- </tbody>
- <tbody class$="[[computeLoadingClass(_loading)]]">
- <template is="dom-repeat" items="[[_shownGroups]]">
- <tr class="table">
- <td class="name">
- <a href$="[[_computeGroupUrl(item.id)]]">[[item.name]]</a>
- </td>
- <td class="description">[[item.description]]</td>
- <td class="visibleToAll">[[_visibleToAll(item)]]</td>
- </tr>
- </template>
- </tbody>
- </table>
- </gr-list-view>
- <gr-overlay id="createOverlay" with-backdrop="">
- <gr-dialog
- id="createDialog"
- class="confirmDialog"
- disabled="[[!_hasNewGroupName]]"
- confirm-label="Create"
- confirm-on-enter=""
- on-confirm="_handleCreateGroup"
- on-cancel="_handleCloseCreate"
- >
- <div class="header" slot="header">Create Group</div>
- <div class="main" slot="main">
- <gr-create-group-dialog
- has-new-group-name="{{_hasNewGroupName}}"
- id="createNewModal"
- ></gr-create-group-dialog>
- </div>
- </gr-dialog>
- </gr-overlay>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.js b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.js
deleted file mode 100644
index 7b7b959..0000000
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.js
+++ /dev/null
@@ -1,184 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-admin-group-list.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import 'lodash/lodash.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-admin-group-list');
-
-let counter = 0;
-const groupGenerator = () => {
- return {
- name: `test${++counter}`,
- id: '59b92f35489e62c80d1ab1bf0c2d17843038df8b',
- url: '#/admin/groups/uuid-59b92f35489e62c80d1ab1bf0c2d17843038df8b',
- options: {
- visible_to_all: false,
- },
- description: 'Gerrit Site Administrators',
- group_id: 1,
- owner: 'Administrators',
- owner_id: '7ca042f4d5847936fcb90ca91057673157fd06fc',
- };
-};
-
-suite('gr-admin-group-list tests', () => {
- let element;
- let groups;
-
- let value;
-
- setup(() => {
- element = basicFixture.instantiate();
- });
-
- test('_computeGroupUrl', () => {
- let urlStub = sinon.stub(GerritNav, 'getUrlForGroup').callsFake(
- () => '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5');
-
- let group = {
- id: 'e2cd66f88a2db4d391ac068a92d987effbe872f5',
- };
- assert.equal(element._computeGroupUrl(group),
- '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5');
-
- urlStub.restore();
-
- urlStub = sinon.stub(GerritNav, 'getUrlForGroup').callsFake(
- () => '/admin/groups/user/test');
-
- group = {
- id: 'user%2Ftest',
- };
- assert.equal(element._computeGroupUrl(group),
- '/admin/groups/user/test');
-
- urlStub.restore();
- });
-
- suite('list with groups', () => {
- setup(async () => {
- groups = _.times(26, groupGenerator);
- stubRestApi('getGroups').returns(Promise.resolve(groups));
- element._paramsChanged(value);
- await flush();
- });
-
- test('test for test group in the list', () => {
- assert.equal(element._groups[1].name, '1');
- assert.equal(element._groups[1].options.visible_to_all, false);
- });
-
- test('_shownGroups', () => {
- assert.equal(element._shownGroups.length, 25);
- });
-
- test('_maybeOpenCreateOverlay', () => {
- const overlayOpen = sinon.stub(element.$.createOverlay, 'open');
- element._maybeOpenCreateOverlay();
- assert.isFalse(overlayOpen.called);
- const params = {};
- element._maybeOpenCreateOverlay(params);
- assert.isFalse(overlayOpen.called);
- params.openCreateModal = true;
- element._maybeOpenCreateOverlay(params);
- assert.isTrue(overlayOpen.called);
- });
- });
-
- suite('test with less then 25 groups', () => {
- setup(async () => {
- groups = _.times(25, groupGenerator);
- stubRestApi('getGroups').returns(Promise.resolve(groups));
- await element._paramsChanged(value);
- await flush();
- });
-
- test('_shownGroups', () => {
- assert.equal(element._shownGroups.length, 25);
- });
- });
-
- suite('filter', () => {
- test('_paramsChanged', async () => {
- const getGroupsStub = stubRestApi('getGroups');
- getGroupsStub.returns(Promise.resolve(groups));
- const value = {
- filter: 'test',
- offset: 25,
- };
- await element._paramsChanged(value);
- assert.isTrue(getGroupsStub.lastCall.calledWithExactly('test', 25, 25));
- });
- });
-
- suite('loading', () => {
- test('correct contents are displayed', () => {
- assert.isTrue(element._loading);
- assert.equal(element.computeLoadingClass(element._loading), 'loading');
- assert.equal(getComputedStyle(element.$.loading).display, 'block');
-
- element._loading = false;
- element._groups = _.times(25, groupGenerator);
-
- flush();
- assert.equal(element.computeLoadingClass(element._loading), '');
- assert.equal(getComputedStyle(element.$.loading).display, 'none');
- });
- });
-
- suite('create new', () => {
- test('_handleCreateClicked called when create-click fired', () => {
- sinon.stub(element, '_handleCreateClicked');
- element.shadowRoot
- .querySelector('gr-list-view').dispatchEvent(
- new CustomEvent('create-clicked', {
- composed: true, bubbles: true,
- }));
- assert.isTrue(element._handleCreateClicked.called);
- });
-
- test('_handleCreateClicked opens modal', () => {
- const openStub = sinon.stub(element.$.createOverlay, 'open').returns(
- Promise.resolve());
- element._handleCreateClicked();
- assert.isTrue(openStub.called);
- });
-
- test('_handleCreateGroup called when confirm fired', () => {
- sinon.stub(element, '_handleCreateGroup');
- element.$.createDialog.dispatchEvent(
- new CustomEvent('confirm', {
- composed: true, bubbles: true,
- }));
- assert.isTrue(element._handleCreateGroup.called);
- });
-
- test('_handleCloseCreate called when cancel fired', () => {
- sinon.stub(element, '_handleCloseCreate');
- element.$.createDialog.dispatchEvent(
- new CustomEvent('cancel', {
- composed: true, bubbles: true,
- }));
- assert.isTrue(element._handleCloseCreate.called);
- });
- });
-});
-
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
new file mode 100644
index 0000000..709a0b7
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts
@@ -0,0 +1,229 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-admin-group-list';
+import {GrAdminGroupList} from './gr-admin-group-list';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
+import {
+ GroupId,
+ GroupName,
+ GroupNameToGroupInfoMap,
+} from '../../../types/common';
+import {AppElementAdminParams} from '../../gr-app-types';
+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';
+
+const basicFixture = fixtureFromElement('gr-admin-group-list');
+
+function createGroup(name: string, counter: number) {
+ return {
+ name: `${name}${counter}` as GroupName,
+ id: '59b92f35489e62c80d1ab1bf0c2d17843038df8b' as GroupId,
+ url: '#/admin/groups/uuid-59b92f35489e62c80d1ab1bf0c2d17843038df8b',
+ options: {
+ visible_to_all: false,
+ },
+ description: 'Gerrit Site Administrators',
+ group_id: 1,
+ owner: 'Administrators',
+ owner_id: '7ca042f4d5847936fcb90ca91057673157fd06fc',
+ };
+}
+
+function createGroupList(name: string, n: number) {
+ const groups = [];
+ for (let i = 0; i < n; ++i) {
+ groups.push(createGroup(name, i));
+ }
+ return groups;
+}
+
+function createGroupObjectList(name: string, n: number) {
+ const groups: GroupNameToGroupInfoMap = {};
+ for (let i = 0; i < n; ++i) {
+ groups[`${name}${i}`] = createGroup(name, i);
+ }
+ return groups;
+}
+
+suite('gr-admin-group-list tests', () => {
+ let element: GrAdminGroupList;
+ let groups: GroupNameToGroupInfoMap;
+
+ const value: AppElementAdminParams = {view: GerritView.ADMIN, adminView: ''};
+
+ setup(async () => {
+ element = basicFixture.instantiate();
+ await element.updateComplete;
+ });
+
+ test('computeGroupUrl', () => {
+ let urlStub = sinon
+ .stub(GerritNav, 'getUrlForGroup')
+ .callsFake(
+ () => '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5'
+ );
+
+ let group = 'e2cd66f88a2db4d391ac068a92d987effbe872f5';
+ assert.equal(
+ element.computeGroupUrl(group),
+ '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5'
+ );
+
+ urlStub.restore();
+
+ urlStub = sinon
+ .stub(GerritNav, 'getUrlForGroup')
+ .callsFake(() => '/admin/groups/user/test');
+
+ group = 'user%2Ftest';
+ assert.equal(element.computeGroupUrl(group), '/admin/groups/user/test');
+
+ urlStub.restore();
+ });
+
+ suite('list with groups', () => {
+ setup(async () => {
+ groups = createGroupObjectList('test', 26);
+ stubRestApi('getGroups').returns(Promise.resolve(groups));
+ element.params = value;
+ element.paramsChanged();
+ await element.updateComplete;
+ });
+
+ test('test for test group in the list', () => {
+ assert.equal(element.groups[1].name, 'test1' as GroupName);
+ assert.equal(element.groups[1].options!.visible_to_all, false);
+ });
+
+ test('groups', () => {
+ assert.equal(element.groups.slice(0, SHOWN_ITEMS_COUNT).length, 25);
+ });
+
+ test('maybeOpenCreateOverlay', () => {
+ const overlayOpen = sinon.stub(
+ queryAndAssert<GrOverlay>(element, '#createOverlay'),
+ 'open'
+ );
+ element.maybeOpenCreateOverlay();
+ assert.isFalse(overlayOpen.called);
+ element.maybeOpenCreateOverlay(undefined);
+ assert.isFalse(overlayOpen.called);
+ value.openCreateModal = true;
+ element.maybeOpenCreateOverlay(value);
+ assert.isTrue(overlayOpen.called);
+ });
+ });
+
+ suite('test with 25 groups', () => {
+ setup(async () => {
+ groups = createGroupObjectList('test', 25);
+ stubRestApi('getGroups').returns(Promise.resolve(groups));
+ element.params = value;
+ await element.paramsChanged();
+ await element.updateComplete;
+ });
+
+ test('groups', () => {
+ assert.equal(element.groups.slice(0, SHOWN_ITEMS_COUNT).length, 25);
+ });
+ });
+
+ suite('filter', () => {
+ test('paramsChanged', async () => {
+ const getGroupsStub = stubRestApi('getGroups');
+ getGroupsStub.returns(Promise.resolve(groups));
+ value.filter = 'test';
+ value.offset = 25;
+ element.params = value;
+ await element.paramsChanged();
+ assert.isTrue(getGroupsStub.lastCall.calledWithExactly('test', 25, 25));
+ });
+ });
+
+ suite('loading', async () => {
+ test('correct contents are displayed', async () => {
+ assert.isTrue(element.loading);
+ assert.equal(
+ getComputedStyle(queryAndAssert<HTMLTableElement>(element, '#loading'))
+ .display,
+ 'block'
+ );
+
+ element.loading = false;
+ element.groups = createGroupList('test', 25);
+
+ await element.updateComplete;
+ assert.equal(
+ getComputedStyle(queryAndAssert<HTMLTableElement>(element, '#loading'))
+ .display,
+ 'none'
+ );
+ });
+ });
+
+ suite('create new', () => {
+ test('handleCreateClicked called when create-click fired', () => {
+ const handleCreateClickedStub = sinon.stub(
+ element,
+ 'handleCreateClicked'
+ );
+ queryAndAssert<GrListView>(element, 'gr-list-view').dispatchEvent(
+ new CustomEvent('create-clicked', {
+ composed: true,
+ bubbles: true,
+ })
+ );
+ assert.isTrue(handleCreateClickedStub.called);
+ });
+
+ test('handleCreateClicked opens modal', () => {
+ const openStub = sinon
+ .stub(queryAndAssert<GrOverlay>(element, '#createOverlay'), 'open')
+ .returns(Promise.resolve());
+ element.handleCreateClicked();
+ assert.isTrue(openStub.called);
+ });
+
+ test('handleCreateGroup called when confirm fired', () => {
+ const handleCreateGroupStub = sinon.stub(element, 'handleCreateGroup');
+ queryAndAssert<GrDialog>(element, '#createDialog').dispatchEvent(
+ new CustomEvent('confirm', {
+ composed: true,
+ bubbles: true,
+ })
+ );
+ assert.isTrue(handleCreateGroupStub.called);
+ });
+
+ test('handleCloseCreate called when cancel fired', () => {
+ const handleCloseCreateStub = sinon.stub(element, 'handleCloseCreate');
+ queryAndAssert<GrDialog>(element, '#createDialog').dispatchEvent(
+ new CustomEvent('cancel', {
+ composed: true,
+ bubbles: true,
+ })
+ );
+ assert.isTrue(handleCloseCreateStub.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 1c9c6f3..10ca808 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
@@ -14,9 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/gr-menu-page-styles';
-import '../../../styles/gr-page-nav-styles';
-import '../../../styles/shared-styles';
+
import '../../shared/gr-dropdown-list/gr-dropdown-list';
import '../../shared/gr-icons/gr-icons';
import '../../shared/gr-page-nav/gr-page-nav';
@@ -31,8 +29,6 @@
import '../gr-repo-dashboards/gr-repo-dashboards';
import '../gr-repo-detail-list/gr-repo-detail-list';
import '../gr-repo-list/gr-repo-list';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-admin-view_html';
import {getBaseUrl} from '../../../utils/url-util';
import {
GerritNav,
@@ -46,7 +42,6 @@
NavLink,
SubsectionInterface,
} from '../../../utils/admin-nav-util';
-import {customElement, observe, property} from '@polymer/decorators';
import {
AppElementAdminParams,
AppElementGroupParams,
@@ -60,12 +55,18 @@
} from '../../../types/common';
import {GroupNameChangedDetail} from '../gr-group/gr-group';
import {ValueChangeDetail} from '../../shared/gr-dropdown-list/gr-dropdown-list';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {GerritView} 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';
+import {LitElement, PropertyValues, css, html} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+import {ifDefined} from 'lit/directives/if-defined';
const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
-interface AdminSubsectionLink {
+export interface AdminSubsectionLink {
text: string;
value: string;
view: GerritView;
@@ -90,11 +91,7 @@
}
@customElement('gr-admin-view')
-export class GrAdminView extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrAdminView extends LitElement {
private account?: AccountDetailInfo;
@property({type: Object})
@@ -106,273 +103,508 @@
@property({type: String})
adminView?: string;
- @property({type: String})
- _breadcrumbParentName?: string;
+ @state() private breadcrumbParentName?: string;
- @property({type: String})
- _repoName?: RepoName;
+ // private but used in test
+ @state() repoName?: RepoName;
- @property({type: String, observer: '_computeGroupName'})
- _groupId?: GroupId;
+ // private but used in test
+ @state() groupId?: GroupId;
- @property({type: Boolean})
- _groupIsInternal?: boolean;
+ // private but used in test
+ @state() groupIsInternal?: boolean;
- @property({type: String})
- _groupName?: GroupName;
+ // private but used in test
+ @state() groupName?: GroupName;
- @property({type: Boolean})
- _groupOwner = false;
+ // private but used in test
+ @state() subsectionLinks?: AdminSubsectionLink[];
- @property({type: Array})
- _subsectionLinks?: AdminSubsectionLink[];
+ // private but used in test
+ @state() filteredLinks?: NavLink[];
- @property({type: Array})
- _filteredLinks?: NavLink[];
+ // private but used in the tests
+ readonly jsAPI = getAppContext().jsApiService;
- @property({type: Boolean})
- _showDownload = false;
-
- @property({type: Boolean})
- _isAdmin = false;
-
- @property({type: Boolean})
- _showGroup?: boolean;
-
- @property({type: Boolean})
- _showGroupAuditLog?: boolean;
-
- @property({type: Boolean})
- _showGroupList?: boolean;
-
- @property({type: Boolean})
- _showGroupMembers?: boolean;
-
- @property({type: Boolean})
- _showRepoAccess?: boolean;
-
- @property({type: Boolean})
- _showRepoCommands?: boolean;
-
- @property({type: Boolean})
- _showRepoDashboards?: boolean;
-
- @property({type: Boolean})
- _showRepoDetailList?: boolean;
-
- @property({type: Boolean})
- _showRepoMain?: boolean;
-
- @property({type: Boolean})
- _showRepoList?: boolean;
-
- @property({type: Boolean})
- _showPluginList?: boolean;
-
- private readonly restApiService = appContext.restApiService;
-
- private readonly jsAPI = appContext.jsApiService;
+ private readonly restApiService = getAppContext().restApiService;
override connectedCallback() {
super.connectedCallback();
this.reload();
}
- reload() {
+ static override get styles() {
+ return [
+ sharedStyles,
+ menuPageStyles,
+ pageNavStyles,
+ css`
+ .breadcrumbText {
+ /* Same as dropdown trigger so chevron spacing is consistent. */
+ padding: 5px 4px;
+ }
+ iron-icon {
+ margin: 0 var(--spacing-xs);
+ }
+ .breadcrumb {
+ align-items: center;
+ display: flex;
+ }
+ .mainHeader {
+ align-items: baseline;
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ }
+ .selectText {
+ display: none;
+ }
+ .selectText.show {
+ display: inline-block;
+ }
+ .main.breadcrumbs:not(.table) {
+ margin-top: var(--spacing-l);
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`
+ <gr-page-nav class="navStyles">
+ <ul class="sectionContent">
+ ${this.filteredLinks?.map(item => this.renderAdminNav(item))}
+ </ul>
+ </gr-page-nav>
+ ${this.renderSubsectionLinks()} ${this.renderRepoList()}
+ ${this.renderGroupList()} ${this.renderPluginList()}
+ ${this.renderRepoMain()} ${this.renderGroup()}
+ ${this.renderGroupMembers()} ${this.renderGroupAuditLog()}
+ ${this.renderRepoDetailList()} ${this.renderRepoCommands()}
+ ${this.renderRepoAccess()} ${this.renderRepoDashboards()}
+ `;
+ }
+
+ private renderAdminNav(item: NavLink) {
+ return html`
+ <li class="sectionTitle ${this.computeSelectedClass(item.view)}">
+ <a class="title" href="${this.computeLinkURL(item)}" rel="noopener"
+ >${item.name}</a
+ >
+ </li>
+ ${item.children?.map(child => this.renderAdminNavChild(child))}
+ ${this.renderAdminNavSubsection(item)}
+ `;
+ }
+
+ private renderAdminNavChild(child: SubsectionInterface) {
+ return html`
+ <li class="${this.computeSelectedClass(child.view)}">
+ <a href="${this.computeLinkURL(child)}" rel="noopener">${child.name}</a>
+ </li>
+ `;
+ }
+
+ private renderAdminNavSubsection(item: NavLink) {
+ if (!item.subsection) return;
+
+ return html`
+ <!--If a section has a subsection, render that.-->
+ <li class="${this.computeSelectedClass(item.subsection.view)}">
+ ${this.renderAdminNavSubsectionUrl(item.subsection)}
+ </li>
+ <!--Loop through the links in the sub-section.-->
+ ${item.subsection?.children?.map(child =>
+ this.renderAdminNavSubsectionChild(child)
+ )}
+ `;
+ }
+
+ private renderAdminNavSubsectionUrl(subsection?: SubsectionInterface) {
+ if (!subsection!.url) return html`${subsection!.name}`;
+
+ return html`
+ <a class="title" href="${this.computeLinkURL(subsection)}" rel="noopener">
+ ${subsection!.name}</a
+ >
+ `;
+ }
+
+ private renderAdminNavSubsectionChild(child: SubsectionInterface) {
+ return html`
+ <li
+ class="subsectionItem ${this.computeSelectedClass(
+ child.view,
+ child.detailType
+ )}"
+ >
+ <a href="${this.computeLinkURL(child)}">${child.name}</a>
+ </li>
+ `;
+ }
+
+ private renderSubsectionLinks() {
+ if (!this.subsectionLinks?.length) return;
+
+ return html`
+ <section class="mainHeader">
+ <span class="breadcrumb">
+ <span class="breadcrumbText">${this.breadcrumbParentName}</span>
+ <iron-icon icon="gr-icons:chevron-right"></iron-icon>
+ </span>
+ <gr-dropdown-list
+ id="pageSelect"
+ value=${ifDefined(this.computeSelectValue())}
+ .items=${this.subsectionLinks}
+ @value-change=${this.handleSubsectionChange}
+ >
+ </gr-dropdown-list>
+ </section>
+ `;
+ }
+
+ private renderRepoList() {
+ const params = this.params as AppElementAdminParams;
+ if (
+ !(
+ params?.view === GerritView.ADMIN &&
+ params?.adminView === 'gr-repo-list'
+ )
+ )
+ return;
+
+ return html`
+ <div class="main table">
+ <gr-repo-list class="table" .params=${params}></gr-repo-list>
+ </div>
+ `;
+ }
+
+ private renderGroupList() {
+ const params = this.params as AppElementAdminParams;
+ if (
+ !(
+ params?.view === GerritView.ADMIN &&
+ params?.adminView === 'gr-admin-group-list'
+ )
+ )
+ return;
+
+ return html`
+ <div class="main table">
+ <gr-admin-group-list class="table" .params=${params}>
+ </gr-admin-group-list>
+ </div>
+ `;
+ }
+
+ private renderPluginList() {
+ const params = this.params as AppElementAdminParams;
+ if (
+ !(
+ params?.view === GerritView.ADMIN &&
+ params?.adminView === 'gr-plugin-list'
+ )
+ )
+ return;
+
+ return html`
+ <div class="main table">
+ <gr-plugin-list class="table" .params=${params}></gr-plugin-list>
+ </div>
+ `;
+ }
+
+ private renderRepoMain() {
+ const params = this.params as AppElementRepoParams;
+ if (
+ !(
+ params?.view === GerritView.REPO &&
+ (!params?.detail || params?.detail === RepoDetailView.GENERAL)
+ )
+ )
+ return;
+
+ return html`
+ <div class="main breadcrumbs">
+ <gr-repo .repo=${params.repo}></gr-repo>
+ </div>
+ `;
+ }
+
+ private renderGroup() {
+ const params = this.params as AppElementGroupParams;
+ if (!(params?.view === GerritView.GROUP && !params?.detail)) return;
+
+ return html`
+ <div class="main breadcrumbs">
+ <gr-group
+ .groupId=${params.groupId}
+ @name-changed=${(e: CustomEvent<GroupNameChangedDetail>) => {
+ this.updateGroupName(e);
+ }}
+ ></gr-group>
+ </div>
+ `;
+ }
+
+ private renderGroupMembers() {
+ const params = this.params as AppElementGroupParams;
+ if (
+ !(
+ params?.view === GerritView.GROUP &&
+ params?.detail === GroupDetailView.MEMBERS
+ )
+ )
+ return;
+
+ return html`
+ <div class="main breadcrumbs">
+ <gr-group-members .groupId=${params.groupId}></gr-group-members>
+ </div>
+ `;
+ }
+
+ private renderGroupAuditLog() {
+ const params = this.params as AppElementGroupParams;
+ if (
+ !(
+ params?.view === GerritView.GROUP &&
+ params?.detail === GroupDetailView.LOG
+ )
+ )
+ return;
+
+ return html`
+ <div class="main table breadcrumbs">
+ <gr-group-audit-log
+ class="table"
+ .groupId=${params.groupId}
+ ></gr-group-audit-log>
+ </div>
+ `;
+ }
+
+ private renderRepoDetailList() {
+ const params = this.params as AppElementRepoParams;
+ if (
+ !(
+ params?.view === GerritView.REPO &&
+ (params?.detail === RepoDetailView.BRANCHES ||
+ params?.detail === RepoDetailView.TAGS)
+ )
+ )
+ return;
+
+ return html`
+ <div class="main table breadcrumbs">
+ <gr-repo-detail-list
+ class="table"
+ .params=${params}
+ ></gr-repo-detail-list>
+ </div>
+ `;
+ }
+
+ private renderRepoCommands() {
+ const params = this.params as AppElementRepoParams;
+ if (
+ !(
+ params?.view === GerritView.REPO &&
+ params?.detail === RepoDetailView.COMMANDS
+ )
+ )
+ return;
+
+ return html`
+ <div class="main breadcrumbs">
+ <gr-repo-commands .repo=${params.repo}></gr-repo-commands>
+ </div>
+ `;
+ }
+
+ private renderRepoAccess() {
+ const params = this.params as AppElementRepoParams;
+ if (
+ !(
+ params?.view === GerritView.REPO &&
+ params?.detail === RepoDetailView.ACCESS
+ )
+ )
+ return;
+
+ return html`
+ <div class="main breadcrumbs">
+ <gr-repo-access
+ .path=${this.path}
+ .repo=${params.repo}
+ ></gr-repo-access>
+ </div>
+ `;
+ }
+
+ private renderRepoDashboards() {
+ const params = this.params as AppElementRepoParams;
+ if (
+ !(
+ params?.view === GerritView.REPO &&
+ params?.detail === RepoDetailView.DASHBOARDS
+ )
+ )
+ return;
+
+ return html`
+ <div class="main table breadcrumbs">
+ <gr-repo-dashboards .repo=${params.repo}></gr-repo-dashboards>
+ </div>
+ `;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('params')) {
+ this.paramsChanged();
+ }
+
+ if (changedProperties.has('groupId')) {
+ this.computeGroupName();
+ }
+ }
+
+ async reload() {
const promises: [Promise<AccountDetailInfo | undefined>, Promise<void>] = [
this.restApiService.getAccount(),
getPluginLoader().awaitPluginsLoaded(),
];
- return Promise.all(promises).then(result => {
- this.account = result[0];
- let options: AdminNavLinksOption | undefined = undefined;
- if (this._repoName) {
- options = {repoName: this._repoName};
- } else if (this._groupId) {
- options = {
- groupId: this._groupId,
- groupName: this._groupName,
- groupIsInternal: this._groupIsInternal,
- isAdmin: this._isAdmin,
- groupOwner: this._groupOwner,
+ const result = await Promise.all(promises);
+ this.account = result[0];
+ let options: AdminNavLinksOption | undefined = undefined;
+ if (this.repoName) {
+ options = {repoName: this.repoName};
+ } else if (this.groupId) {
+ const isAdmin = await this.restApiService.getIsAdmin();
+ const isOwner = await this.restApiService.getIsGroupOwner(this.groupName);
+ options = {
+ groupId: this.groupId,
+ groupName: this.groupName,
+ groupIsInternal: this.groupIsInternal,
+ isAdmin,
+ groupOwner: isOwner,
+ };
+ }
+
+ const res = await getAdminLinks(
+ this.account,
+ () =>
+ this.restApiService.getAccountCapabilities().then(capabilities => {
+ if (!capabilities) {
+ throw new Error('getAccountCapabilities returns undefined');
+ }
+ return capabilities;
+ }),
+ () => this.jsAPI.getAdminMenuLinks(),
+ options
+ );
+ this.filteredLinks = res.links;
+ this.breadcrumbParentName = res.expandedSection
+ ? res.expandedSection.name
+ : '';
+
+ if (!res.expandedSection) {
+ this.subsectionLinks = [];
+ return;
+ }
+ this.subsectionLinks = [res.expandedSection]
+ .concat(res.expandedSection.children ?? [])
+ .map(section => {
+ return {
+ text: !section.detailType ? 'Home' : section.name,
+ value: section.view + (section.detailType ?? ''),
+ view: section.view,
+ url: section.url,
+ detailType: section.detailType,
+ parent: this.groupId ?? this.repoName,
};
- }
-
- return getAdminLinks(
- this.account,
- () =>
- this.restApiService.getAccountCapabilities().then(capabilities => {
- if (!capabilities) {
- throw new Error('getAccountCapabilities returns undefined');
- }
- return capabilities;
- }),
- () => this.jsAPI.getAdminMenuLinks(),
- options
- ).then(res => {
- this._filteredLinks = res.links;
- this._breadcrumbParentName = res.expandedSection
- ? res.expandedSection.name
- : '';
-
- if (!res.expandedSection) {
- this._subsectionLinks = [];
- return;
- }
- this._subsectionLinks = [res.expandedSection]
- .concat(res.expandedSection.children ?? [])
- .map(section => {
- return {
- text: !section.detailType ? 'Home' : section.name,
- value: section.view + (section.detailType ?? ''),
- view: section.view,
- url: section.url,
- detailType: section.detailType,
- parent: this._groupId ?? this._repoName,
- };
- });
});
- });
}
- _computeSelectValue(params: AdminViewParams) {
- if (!params || !params.view) return;
- return `${params.view}${getAdminViewParamsDetail(params) ?? ''}`;
+ private computeSelectValue() {
+ if (!this.params?.view) return;
+ return `${this.params.view}${getAdminViewParamsDetail(this.params) ?? ''}`;
}
- _selectedIsCurrentPage(selected: AdminSubsectionLink) {
+ // private but used in test
+ selectedIsCurrentPage(selected: AdminSubsectionLink) {
if (!this.params) return false;
return (
- selected.parent === (this._repoName ?? this._groupId) &&
+ selected.parent === (this.repoName ?? this.groupId) &&
selected.view === this.params.view &&
selected.detailType === getAdminViewParamsDetail(this.params)
);
}
- _handleSubsectionChange(e: CustomEvent<ValueChangeDetail>) {
- if (!this._subsectionLinks) return;
+ // private but used in test
+ handleSubsectionChange(e: CustomEvent<ValueChangeDetail>) {
+ if (!this.subsectionLinks) return;
- // The GrDropdownList items are _subsectionLinks, so find(...) always return
- // an item _subsectionLinks and never returns undefined
- const selected = this._subsectionLinks.find(
+ // The GrDropdownList items are subsectionLinks, so find(...) always return
+ // an item subsectionLinks and never returns undefined
+ const selected = this.subsectionLinks.find(
section => section.value === e.detail.value
)!;
// This is when it gets set initially.
- if (this._selectedIsCurrentPage(selected)) return;
+ if (this.selectedIsCurrentPage(selected)) return;
if (selected.url === undefined) return;
GerritNav.navigateToRelativeUrl(selected.url);
}
- @observe('params')
- _paramsChanged(params: AdminViewParams) {
- this.set('_showGroup', params.view === GerritView.GROUP && !params.detail);
- this.set(
- '_showGroupAuditLog',
- params.view === GerritView.GROUP && params.detail === GroupDetailView.LOG
- );
- this.set(
- '_showGroupMembers',
- params.view === GerritView.GROUP &&
- params.detail === GroupDetailView.MEMBERS
- );
+ private async paramsChanged() {
+ if (this.needsReload()) await this.reload();
+ }
- this.set(
- '_showGroupList',
- params.view === GerritView.ADMIN &&
- params.adminView === 'gr-admin-group-list'
- );
+ needsReload() {
+ if (!this.params) return;
- this.set(
- '_showRepoAccess',
- params.view === GerritView.REPO && params.detail === RepoDetailView.ACCESS
- );
- this.set(
- '_showRepoCommands',
- params.view === GerritView.REPO &&
- params.detail === RepoDetailView.COMMANDS
- );
- this.set(
- '_showRepoDetailList',
- params.view === GerritView.REPO &&
- (params.detail === RepoDetailView.BRANCHES ||
- params.detail === RepoDetailView.TAGS)
- );
- this.set(
- '_showRepoDashboards',
- params.view === GerritView.REPO &&
- params.detail === RepoDetailView.DASHBOARDS
- );
- this.set(
- '_showRepoMain',
- params.view === GerritView.REPO &&
- (!params.detail || params.detail === RepoDetailView.GENERAL)
- );
- this.set(
- '_showRepoList',
- params.view === GerritView.ADMIN && params.adminView === 'gr-repo-list'
- );
-
- this.set(
- '_showPluginList',
- params.view === GerritView.ADMIN && params.adminView === 'gr-plugin-list'
- );
-
- let needsReload = false;
const newRepoName =
- params.view === GerritView.REPO ? params.repo : undefined;
- if (newRepoName !== this._repoName) {
- this._repoName = newRepoName;
+ this.params.view === GerritView.REPO ? this.params.repo : undefined;
+ if (newRepoName !== this.repoName) {
+ this.repoName = newRepoName;
// Reloads the admin menu.
- needsReload = true;
+ return true;
}
const newGroupId =
- params.view === GerritView.GROUP ? params.groupId : undefined;
- if (newGroupId !== this._groupId) {
- this._groupId = newGroupId;
+ this.params.view === GerritView.GROUP ? this.params.groupId : undefined;
+ if (newGroupId !== this.groupId) {
+ this.groupId = newGroupId;
// Reloads the admin menu.
- needsReload = true;
+ return true;
}
if (
- this._breadcrumbParentName &&
- (params.view !== GerritView.GROUP || !params.groupId) &&
- (params.view !== GerritView.REPO || !params.repo)
+ this.breadcrumbParentName &&
+ (this.params.view !== GerritView.GROUP || !this.params.groupId) &&
+ (this.params.view !== GerritView.REPO || !this.params.repo)
) {
- needsReload = true;
+ return true;
}
- if (!needsReload) {
- return;
- }
- this.reload();
+
+ return false;
}
- // TODO (beckysiegel): Update these functions after router abstraction is
- // updated. They are currently copied from gr-dropdown (and should be
- // updated there as well once complete).
- _computeURLHelper(host: string, path: string) {
- return '//' + host + getBaseUrl() + path;
- }
-
- _computeRelativeURL(path: string) {
- const host = window.location.host;
- return this._computeURLHelper(host, path);
- }
-
- _computeLinkURL(link: NavLink | SubsectionInterface) {
+ // private but used in test
+ computeLinkURL(link?: NavLink | SubsectionInterface) {
if (!link || typeof link.url === 'undefined') return '';
if ((link as NavLink).target || !(link as NavLink).noBaseUrl) {
return link.url;
}
- return this._computeRelativeURL(link.url);
+ return `//${window.location.host}${getBaseUrl()}${link.url}`;
}
- _computeSelectedClass(
+ private computeSelectedClass(
itemView?: GerritView,
- params?: AdminViewParams,
detailType?: GroupDetailView | RepoDetailView
) {
+ const params = this.params;
if (!params) return '';
// Group params are structured differently from admin params. Compute
// selected differently for groups.
@@ -409,40 +641,23 @@
: '';
}
- _computeGroupName(groupId?: GroupId) {
- if (!groupId) return;
+ // private but used in test
+ async computeGroupName() {
+ if (!this.groupId) return;
- const promises: Array<Promise<void>> = [];
- this.restApiService.getGroupConfig(groupId).then(group => {
- if (!group || !group.name) {
- return;
- }
+ const group = await this.restApiService.getGroupConfig(this.groupId);
+ if (!group || !group.name) {
+ return;
+ }
- this._groupName = group.name;
- this._groupIsInternal = !!group.id.match(INTERNAL_GROUP_REGEX);
- this.reload();
-
- promises.push(
- this.restApiService.getIsAdmin().then(isAdmin => {
- this._isAdmin = !!isAdmin;
- })
- );
-
- promises.push(
- this.restApiService.getIsGroupOwner(group.name).then(isOwner => {
- this._groupOwner = isOwner;
- })
- );
-
- return Promise.all(promises).then(() => {
- this.reload();
- });
- });
+ this.groupName = group.name;
+ this.groupIsInternal = !!group.id.match(INTERNAL_GROUP_REGEX);
+ await this.reload();
}
- _updateGroupName(e: CustomEvent<GroupNameChangedDetail>) {
- this._groupName = e.detail.name;
- this.reload();
+ private async updateGroupName(e: CustomEvent<GroupNameChangedDetail>) {
+ this.groupName = e.detail.name;
+ await this.reload();
}
}
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.ts
deleted file mode 100644
index a3afc5c..0000000
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.ts
+++ /dev/null
@@ -1,181 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-menu-page-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-page-nav-styles">
- .breadcrumbText {
- /* Same as dropdown trigger so chevron spacing is consistent. */
- padding: 5px 4px;
- }
- iron-icon {
- margin: 0 var(--spacing-xs);
- }
- .breadcrumb {
- align-items: center;
- display: flex;
- }
- .mainHeader {
- align-items: baseline;
- border-bottom: 1px solid var(--border-color);
- display: flex;
- }
- .selectText {
- display: none;
- }
- .selectText.show {
- display: inline-block;
- }
- .main.breadcrumbs:not(.table) {
- margin-top: var(--spacing-l);
- }
- </style>
- <gr-page-nav class="navStyles">
- <ul class="sectionContent">
- <template id="adminNav" is="dom-repeat" items="[[_filteredLinks]]">
- <li class$="sectionTitle [[_computeSelectedClass(item.view, params)]]">
- <a class="title" href="[[_computeLinkURL(item)]]" rel="noopener"
- >[[item.name]]</a
- >
- </li>
- <template is="dom-repeat" items="[[item.children]]" as="child">
- <li class$="[[_computeSelectedClass(child.view, params)]]">
- <a href$="[[_computeLinkURL(child)]]" rel="noopener"
- >[[child.name]]</a
- >
- </li>
- </template>
- <template is="dom-if" if="[[item.subsection]]">
- <!--If a section has a subsection, render that.-->
- <li class$="[[_computeSelectedClass(item.subsection.view, params)]]">
- <template is="dom-if" if="[[item.subsection.url]]" as="child">
- <a
- class="title"
- href$="[[_computeLinkURL(item.subsection)]]"
- rel="noopener"
- >
- [[item.subsection.name]]</a
- >
- </template>
- <template is="dom-if" if="[[!item.subsection.url]]" as="child">
- [[item.subsection.name]]
- </template>
- </li>
- <!--Loop through the links in the sub-section.-->
- <template
- is="dom-repeat"
- items="[[item.subsection.children]]"
- as="child"
- >
- <li
- class$="subsectionItem [[_computeSelectedClass(child.view, params, child.detailType)]]"
- >
- <a href$="[[_computeLinkURL(child)]]">[[child.name]]</a>
- </li>
- </template>
- </template>
- </template>
- </ul>
- </gr-page-nav>
- <template is="dom-if" if="[[_subsectionLinks.length]]">
- <section class="mainHeader">
- <span class="breadcrumb">
- <span class="breadcrumbText">[[_breadcrumbParentName]]</span>
- <iron-icon icon="gr-icons:chevron-right"></iron-icon>
- </span>
- <gr-dropdown-list
- lowercase=""
- id="pageSelect"
- value="[[_computeSelectValue(params)]]"
- items="[[_subsectionLinks]]"
- on-value-change="_handleSubsectionChange"
- >
- </gr-dropdown-list>
- </section>
- </template>
- <template is="dom-if" if="[[_showRepoList]]" restamp="true">
- <div class="main table">
- <gr-repo-list class="table" params="[[params]]"></gr-repo-list>
- </div>
- </template>
- <template is="dom-if" if="[[_showGroupList]]" restamp="true">
- <div class="main table">
- <gr-admin-group-list class="table" params="[[params]]">
- </gr-admin-group-list>
- </div>
- </template>
- <template is="dom-if" if="[[_showPluginList]]" restamp="true">
- <div class="main table">
- <gr-plugin-list class="table" params="[[params]]"></gr-plugin-list>
- </div>
- </template>
- <template is="dom-if" if="[[_showRepoMain]]" restamp="true">
- <div class="main breadcrumbs">
- <gr-repo repo="[[params.repo]]"></gr-repo>
- </div>
- </template>
- <template is="dom-if" if="[[_showGroup]]" restamp="true">
- <div class="main breadcrumbs">
- <gr-group
- group-id="[[params.groupId]]"
- on-name-changed="_updateGroupName"
- ></gr-group>
- </div>
- </template>
- <template is="dom-if" if="[[_showGroupMembers]]" restamp="true">
- <div class="main breadcrumbs">
- <gr-group-members group-id="[[params.groupId]]"></gr-group-members>
- </div>
- </template>
- <template is="dom-if" if="[[_showRepoDetailList]]" restamp="true">
- <div class="main table breadcrumbs">
- <gr-repo-detail-list
- params="[[params]]"
- class="table"
- ></gr-repo-detail-list>
- </div>
- </template>
- <template is="dom-if" if="[[_showGroupAuditLog]]" restamp="true">
- <div class="main table breadcrumbs">
- <gr-group-audit-log
- group-id="[[params.groupId]]"
- class="table"
- ></gr-group-audit-log>
- </div>
- </template>
- <template is="dom-if" if="[[_showRepoCommands]]" restamp="true">
- <div class="main breadcrumbs">
- <gr-repo-commands repo="[[params.repo]]"></gr-repo-commands>
- </div>
- </template>
- <template is="dom-if" if="[[_showRepoAccess]]" restamp="true">
- <div class="main breadcrumbs">
- <gr-repo-access path="[[path]]" repo="[[params.repo]]"></gr-repo-access>
- </div>
- </template>
- <template is="dom-if" if="[[_showRepoDashboards]]" restamp="true">
- <div class="main table breadcrumbs">
- <gr-repo-dashboards repo="[[params.repo]]"></gr-repo-dashboards>
- </div>
- </template>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
deleted file mode 100644
index cf0fdd4..0000000
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
+++ /dev/null
@@ -1,594 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-admin-view.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
-import {mockPromise, stubBaseUrl, stubRestApi} from '../../../test/test-utils.js';
-import {GerritView} from '../../../services/router/router-model.js';
-
-const basicFixture = fixtureFromElement('gr-admin-view');
-
-function createAdminCapabilities() {
- return {
- createGroup: true,
- createProject: true,
- viewPlugins: true,
- };
-}
-
-suite('gr-admin-view tests', () => {
- let element;
-
- setup(async () => {
- element = basicFixture.instantiate();
- stubRestApi('getProjectConfig').returns(Promise.resolve({}));
- const pluginsLoaded = Promise.resolve();
- sinon.stub(getPluginLoader(), 'awaitPluginsLoaded').returns(pluginsLoaded);
- await pluginsLoaded;
- await flush();
- });
-
- test('_computeURLHelper', () => {
- const path = '/test';
- const host = 'http://www.testsite.com';
- const computedPath = element._computeURLHelper(host, path);
- assert.equal(computedPath, '//http://www.testsite.com/test');
- });
-
- test('link URLs', () => {
- assert.equal(
- element._computeLinkURL({url: '/test', noBaseUrl: true}),
- '//' + window.location.host + '/test');
-
- stubBaseUrl('/foo');
- assert.equal(
- element._computeLinkURL({url: '/test', noBaseUrl: true}),
- '//' + window.location.host + '/foo/test');
- assert.equal(element._computeLinkURL({url: '/test'}), '/test');
- assert.equal(
- element._computeLinkURL({url: '/test', target: '_blank'}),
- '/test');
- });
-
- test('current page gets selected and is displayed', () => {
- element._filteredLinks = [{
- name: 'Repositories',
- url: '/admin/repos',
- view: 'gr-repo-list',
- }];
-
- element.params = {
- view: 'admin',
- adminView: 'gr-repo-list',
- };
-
- flush();
- assert.equal(element.root.querySelectorAll(
- '.selected').length, 1);
- assert.ok(element.shadowRoot
- .querySelector('gr-repo-list'));
- assert.isNotOk(element.shadowRoot
- .querySelector('gr-admin-create-repo'));
- });
-
- test('_filteredLinks admin', async () => {
- stubRestApi('getAccount').returns(Promise.resolve({
- name: 'test-user',
- }));
- stubRestApi('getAccountCapabilities').returns(
- Promise.resolve(createAdminCapabilities()));
- await element.reload();
- assert.equal(element._filteredLinks.length, 3);
-
- // Repos
- assert.isNotOk(element._filteredLinks[0].subsection);
-
- // Groups
- assert.isNotOk(element._filteredLinks[0].subsection);
-
- // Plugins
- assert.isNotOk(element._filteredLinks[0].subsection);
- });
-
- test('_filteredLinks non admin authenticated', async () => {
- await element.reload();
- assert.equal(element._filteredLinks.length, 2);
- // Repos
- assert.isNotOk(element._filteredLinks[0].subsection);
- // Groups
- assert.isNotOk(element._filteredLinks[0].subsection);
- });
-
- test('_filteredLinks non admin unathenticated', async () => {
- stubRestApi('getAccount').returns(Promise.resolve(undefined));
- await element.reload();
- assert.equal(element._filteredLinks.length, 1);
- // Repos
- assert.isNotOk(element._filteredLinks[0].subsection);
- });
-
- test('_filteredLinks from plugin', () => {
- stubRestApi('getAccount').returns(Promise.resolve(undefined));
- sinon.stub(element.jsAPI, 'getAdminMenuLinks').returns([
- {text: 'internal link text', url: '/internal/link/url'},
- {text: 'external link text', url: 'http://external/link/url'},
- ]);
- return element.reload().then(() => {
- assert.equal(element._filteredLinks.length, 3);
- assert.deepEqual(element._filteredLinks[1], {
- capability: undefined,
- url: '/internal/link/url',
- name: 'internal link text',
- noBaseUrl: true,
- view: null,
- viewableToAll: true,
- target: null,
- });
- assert.deepEqual(element._filteredLinks[2], {
- capability: undefined,
- url: 'http://external/link/url',
- name: 'external link text',
- noBaseUrl: false,
- view: null,
- viewableToAll: true,
- target: '_blank',
- });
- });
- });
-
- test('Repo shows up in nav', async () => {
- element._repoName = 'Test Repo';
- stubRestApi('getAccount').returns(Promise.resolve({
- name: 'test-user',
- }));
- stubRestApi('getAccountCapabilities').returns(
- Promise.resolve(createAdminCapabilities()));
- await element.reload();
- await flush();
- assert.equal(dom(element.root)
- .querySelectorAll('.sectionTitle').length, 3);
- assert.equal(element.shadowRoot
- .querySelector('.breadcrumbText').innerText, 'Test Repo');
- assert.equal(
- element.shadowRoot.querySelector('#pageSelect').items.length,
- 7
- );
- });
-
- test('Group shows up in nav', async () => {
- element._groupId = 'a15262';
- element._groupName = 'my-group';
- element._groupIsInternal = true;
- element._isAdmin = true;
- element._groupOwner = false;
- stubRestApi('getAccount').returns(Promise.resolve({name: 'test-user'}));
- stubRestApi('getAccountCapabilities').returns(
- Promise.resolve(createAdminCapabilities()));
- await element.reload();
- await flush();
- assert.equal(element._filteredLinks.length, 3);
- // Repos
- assert.isNotOk(element._filteredLinks[0].subsection);
- // Groups
- assert.equal(element._filteredLinks[1].subsection.children.length, 2);
- assert.equal(element._filteredLinks[1].subsection.name, 'my-group');
- // Plugins
- assert.isNotOk(element._filteredLinks[2].subsection);
- });
-
- test('Nav is reloaded when repo changes', () => {
- stubRestApi('getAccountCapabilities').returns(
- Promise.resolve(createAdminCapabilities()));
- stubRestApi('getAccount').returns(Promise.resolve({_id: 1}));
- sinon.stub(element, 'reload');
- element.params = {repo: 'Test Repo', view: GerritView.REPO};
- assert.equal(element.reload.callCount, 1);
- element.params = {repo: 'Test Repo 2',
- view: GerritView.REPO};
- assert.equal(element.reload.callCount, 2);
- });
-
- test('Nav is reloaded when group changes', () => {
- sinon.stub(element, '_computeGroupName');
- stubRestApi('getAccountCapabilities').returns(
- Promise.resolve(createAdminCapabilities()));
- stubRestApi('getAccount').returns(Promise.resolve({_id: 1}));
- sinon.stub(element, 'reload');
- element.params = {groupId: '1', view: GerritView.GROUP};
- assert.equal(element.reload.callCount, 1);
- });
-
- test('Nav is reloaded when group name changes', async () => {
- const newName = 'newName';
- const reloadCalled = mockPromise();
- sinon.stub(element, '_computeGroupName');
- sinon.stub(element, 'reload').callsFake(() => {
- assert.equal(element._groupName, newName);
- reloadCalled.resolve();
- });
- element.params = {group: 1, view: GerritNav.View.GROUP};
- element._groupName = 'oldName';
- await flush();
- element.shadowRoot
- .querySelector('gr-group').dispatchEvent(
- new CustomEvent('name-changed', {
- detail: {name: newName},
- composed: true, bubbles: true,
- }));
- await reloadCalled;
- });
-
- test('dropdown displays if there is a subsection', () => {
- assert.isNotOk(element.shadowRoot
- .querySelector('.mainHeader'));
- element._subsectionLinks = [
- {
- text: 'Home',
- value: 'repo',
- view: 'repo',
- parent: 'my-repo',
- detailType: undefined,
- },
- ];
- flush();
- assert.isOk(element.shadowRoot
- .querySelector('.mainHeader'));
- element._subsectionLinks = undefined;
- flush();
- assert.equal(
- getComputedStyle(element.shadowRoot
- .querySelector('.mainHeader')).display,
- 'none');
- });
-
- test('Dropdown only triggers navigation on explicit select', async () => {
- element._repoName = 'my-repo';
- element.params = {
- repo: 'my-repo',
- view: GerritNav.View.REPO,
- detail: GerritNav.RepoDetailView.ACCESS,
- };
- stubRestApi('getAccountCapabilities').returns(
- Promise.resolve(createAdminCapabilities()));
- stubRestApi('getAccount').returns(Promise.resolve({_id: 1}));
- await flush();
- const expectedFilteredLinks = [
- {
- name: 'Repositories',
- noBaseUrl: true,
- url: '/admin/repos',
- view: 'gr-repo-list',
- viewableToAll: true,
- subsection: {
- name: 'my-repo',
- view: 'repo',
- children: [
- {
- name: 'General',
- view: 'repo',
- url: '',
- detailType: 'general',
- },
- {
- name: 'Access',
- view: 'repo',
- detailType: 'access',
- url: '',
- },
- {
- name: 'Commands',
- view: 'repo',
- detailType: 'commands',
- url: '',
- },
- {
- name: 'Branches',
- view: 'repo',
- detailType: 'branches',
- url: '',
- },
- {
- name: 'Tags',
- view: 'repo',
- detailType: 'tags',
- url: '',
- },
- {
- name: 'Dashboards',
- view: 'repo',
- detailType: 'dashboards',
- url: '',
- },
- ],
- },
- },
- {
- name: 'Groups',
- section: 'Groups',
- noBaseUrl: true,
- url: '/admin/groups',
- view: 'gr-admin-group-list',
- },
- {
- name: 'Plugins',
- capability: 'viewPlugins',
- section: 'Plugins',
- noBaseUrl: true,
- url: '/admin/plugins',
- view: 'gr-plugin-list',
- },
- ];
- const expectedSubsectionLinks = [
- {
- text: 'Home',
- value: 'repo',
- view: 'repo',
- url: undefined,
- parent: 'my-repo',
- detailType: undefined,
- },
- {
- text: 'General',
- value: 'repogeneral',
- view: 'repo',
- url: '',
- detailType: 'general',
- parent: 'my-repo',
- },
- {
- text: 'Access',
- value: 'repoaccess',
- view: 'repo',
- url: '',
- detailType: 'access',
- parent: 'my-repo',
- },
- {
- text: 'Commands',
- value: 'repocommands',
- view: 'repo',
- url: '',
- detailType: 'commands',
- parent: 'my-repo',
- },
- {
- text: 'Branches',
- value: 'repobranches',
- view: 'repo',
- url: '',
- detailType: 'branches',
- parent: 'my-repo',
- },
- {
- text: 'Tags',
- value: 'repotags',
- view: 'repo',
- url: '',
- detailType: 'tags',
- parent: 'my-repo',
- },
- {
- text: 'Dashboards',
- value: 'repodashboards',
- view: 'repo',
- url: '',
- detailType: 'dashboards',
- parent: 'my-repo',
- },
- ];
- sinon.stub(GerritNav, 'navigateToRelativeUrl');
- sinon.spy(element, '_selectedIsCurrentPage');
- sinon.spy(element, '_handleSubsectionChange');
- await element.reload();
- assert.deepEqual(element._filteredLinks, expectedFilteredLinks);
- assert.deepEqual(element._subsectionLinks, expectedSubsectionLinks);
- assert.equal(
- element.shadowRoot.querySelector('#pageSelect').value,
- 'repoaccess'
- );
- assert.isTrue(element._selectedIsCurrentPage.calledOnce);
- // Doesn't trigger navigation from the page select menu.
- assert.isFalse(GerritNav.navigateToRelativeUrl.called);
-
- // When explicitly changed, navigation is called
- element.shadowRoot.querySelector('#pageSelect').value = 'repogeneral';
- assert.isTrue(element._selectedIsCurrentPage.calledTwice);
- assert.isTrue(GerritNav.navigateToRelativeUrl.calledOnce);
- });
-
- test('_selectedIsCurrentPage', () => {
- element._repoName = 'my-repo';
- element.params = {view: 'repo', repo: 'my-repo'};
- const selected = {
- view: 'repo',
- detailType: undefined,
- parent: 'my-repo',
- };
- assert.isTrue(element._selectedIsCurrentPage(selected));
- selected.parent = 'my-second-repo';
- assert.isFalse(element._selectedIsCurrentPage(selected));
- selected.detailType = 'detailType';
- assert.isFalse(element._selectedIsCurrentPage(selected));
- });
-
- suite('_computeSelectedClass', () => {
- setup(() => {
- stubRestApi('getAccountCapabilities').returns(
- Promise.resolve(createAdminCapabilities()));
- stubRestApi('getAccount').returns(Promise.resolve({_id: 1}));
- return element.reload();
- });
-
- suite('repos', () => {
- setup(() => {
- stub('gr-repo-access', '_repoChanged').callsFake(() => {});
- });
-
- test('repo list', () => {
- element.params = {
- view: GerritNav.View.ADMIN,
- adminView: 'gr-repo-list',
- openCreateModal: false,
- };
- flush();
- const selected = element.shadowRoot
- .querySelector('gr-page-nav .selected');
- assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'Repositories');
- });
-
- test('repo', () => {
- element.params = {
- view: GerritNav.View.REPO,
- repoName: 'foo',
- };
- element._repoName = 'foo';
- return element.reload().then(() => {
- flush();
- const selected = element.shadowRoot
- .querySelector('gr-page-nav .selected');
- assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'foo');
- });
- });
-
- test('repo access', () => {
- element.params = {
- view: GerritNav.View.REPO,
- detail: GerritNav.RepoDetailView.ACCESS,
- repoName: 'foo',
- };
- element._repoName = 'foo';
- return element.reload().then(() => {
- flush();
- const selected = element.shadowRoot
- .querySelector('gr-page-nav .selected');
- assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'Access');
- });
- });
-
- test('repo dashboards', () => {
- element.params = {
- view: GerritNav.View.REPO,
- detail: GerritNav.RepoDetailView.DASHBOARDS,
- repoName: 'foo',
- };
- element._repoName = 'foo';
- return element.reload().then(() => {
- flush();
- const selected = element.shadowRoot
- .querySelector('gr-page-nav .selected');
- assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'Dashboards');
- });
- });
- });
-
- suite('groups', () => {
- let getGroupConfigStub;
- setup(() => {
- stub('gr-group', '_loadGroup').callsFake(() => Promise.resolve({}));
- stub('gr-group-members', '_loadGroupDetails').callsFake(() => {});
-
- getGroupConfigStub = stubRestApi('getGroupConfig');
- getGroupConfigStub.returns(Promise.resolve({
- name: 'foo',
- id: 'c0f83e941ce90caea30e6ad88f0d4ea0e841a7a9',
- }));
- stubRestApi('getIsGroupOwner')
- .returns(Promise.resolve(true));
- return element.reload();
- });
-
- test('group list', () => {
- element.params = {
- view: GerritNav.View.ADMIN,
- adminView: 'gr-admin-group-list',
- openCreateModal: false,
- };
- flush();
- const selected = element.shadowRoot
- .querySelector('gr-page-nav .selected');
- assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'Groups');
- });
-
- test('internal group', () => {
- element.params = {
- view: GerritNav.View.GROUP,
- groupId: 1234,
- };
- element._groupName = 'foo';
- return element.reload().then(() => {
- flush();
- const subsectionItems = dom(element.root)
- .querySelectorAll('.subsectionItem');
- assert.equal(subsectionItems.length, 2);
- assert.isTrue(element._groupIsInternal);
- const selected = element.shadowRoot
- .querySelector('gr-page-nav .selected');
- assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'foo');
- });
- });
-
- test('external group', () => {
- getGroupConfigStub.returns(Promise.resolve({
- name: 'foo',
- id: 'external-id',
- }));
- element.params = {
- view: GerritNav.View.GROUP,
- groupId: 1234,
- };
- element._groupName = 'foo';
- return element.reload().then(() => {
- flush();
- const subsectionItems = dom(element.root)
- .querySelectorAll('.subsectionItem');
- assert.equal(subsectionItems.length, 0);
- assert.isFalse(element._groupIsInternal);
- const selected = element.shadowRoot
- .querySelector('gr-page-nav .selected');
- assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'foo');
- });
- });
-
- test('group members', () => {
- element.params = {
- view: GerritNav.View.GROUP,
- detail: GerritNav.GroupDetailView.MEMBERS,
- groupId: 1234,
- };
- element._groupName = 'foo';
- return element.reload().then(() => {
- flush();
- const selected = element.shadowRoot
- .querySelector('gr-page-nav .selected');
- assert.isOk(selected);
- assert.equal(selected.textContent.trim(), 'Members');
- });
- });
- });
- });
-});
-
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
new file mode 100644
index 0000000..5574ad1
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
@@ -0,0 +1,649 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-admin-view';
+import {AdminSubsectionLink, GrAdminView} from './gr-admin-view';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
+import {mockPromise, stubBaseUrl, stubRestApi} from '../../../test/test-utils';
+import {GerritView} from '../../../services/router/router-model';
+import {query, queryAll, queryAndAssert} from '../../../test/test-utils';
+import {GrRepoList} from '../gr-repo-list/gr-repo-list';
+import {GroupId, GroupName, RepoName, Timestamp} from '../../../types/common';
+import {GrDropdownList} from '../../shared/gr-dropdown-list/gr-dropdown-list';
+import {GrGroup} from '../gr-group/gr-group';
+
+const basicFixture = fixtureFromElement('gr-admin-view');
+
+function createAdminCapabilities() {
+ return {
+ createGroup: true,
+ createProject: true,
+ viewPlugins: true,
+ };
+}
+
+suite('gr-admin-view tests', () => {
+ let element: GrAdminView;
+
+ setup(async () => {
+ element = basicFixture.instantiate();
+ stubRestApi('getProjectConfig').returns(Promise.resolve(undefined));
+ const pluginsLoaded = Promise.resolve();
+ sinon.stub(getPluginLoader(), 'awaitPluginsLoaded').returns(pluginsLoaded);
+ await pluginsLoaded;
+ await element.updateComplete;
+ });
+
+ test('link URLs', () => {
+ assert.equal(
+ element.computeLinkURL({name: '', url: '/test', noBaseUrl: true}),
+ '//' + window.location.host + '/test'
+ );
+
+ stubBaseUrl('/foo');
+ assert.equal(
+ element.computeLinkURL({name: '', url: '/test', noBaseUrl: true}),
+ '//' + window.location.host + '/foo/test'
+ );
+ assert.equal(
+ element.computeLinkURL({name: '', url: '/test', noBaseUrl: false}),
+ '/test'
+ );
+ assert.equal(
+ element.computeLinkURL({
+ name: '',
+ url: '/test',
+ target: '_blank',
+ noBaseUrl: false,
+ }),
+ '/test'
+ );
+ });
+
+ test('current page gets selected and is displayed', async () => {
+ element.filteredLinks = [
+ {
+ name: 'Repositories',
+ url: '/admin/repos',
+ view: 'gr-repo-list' as GerritView,
+ noBaseUrl: false,
+ },
+ ];
+
+ element.params = {
+ view: GerritView.ADMIN,
+ adminView: 'gr-repo-list',
+ };
+
+ await element.updateComplete;
+ assert.equal(queryAll<HTMLLIElement>(element, '.selected').length, 1);
+ assert.ok(queryAndAssert<GrRepoList>(element, 'gr-repo-list'));
+ assert.isNotOk(query(element, 'gr-admin-create-repo'));
+ });
+
+ test('filteredLinks admin', async () => {
+ stubRestApi('getAccount').returns(
+ Promise.resolve({
+ name: 'test-user',
+ registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+ })
+ );
+ stubRestApi('getAccountCapabilities').returns(
+ Promise.resolve(createAdminCapabilities())
+ );
+ await element.reload();
+ assert.equal(element.filteredLinks!.length, 3);
+
+ // Repos
+ assert.isNotOk(element.filteredLinks![0].subsection);
+
+ // Groups
+ assert.isNotOk(element.filteredLinks![0].subsection);
+
+ // Plugins
+ assert.isNotOk(element.filteredLinks![0].subsection);
+ });
+
+ test('filteredLinks non admin authenticated', async () => {
+ await element.reload();
+ assert.equal(element.filteredLinks!.length, 2);
+ // Repos
+ assert.isNotOk(element.filteredLinks![0].subsection);
+ // Groups
+ assert.isNotOk(element.filteredLinks![0].subsection);
+ });
+
+ test('filteredLinks non admin unathenticated', async () => {
+ stubRestApi('getAccount').returns(Promise.resolve(undefined));
+ await element.reload();
+ assert.equal(element.filteredLinks!.length, 1);
+ // Repos
+ assert.isNotOk(element.filteredLinks![0].subsection);
+ });
+
+ 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'},
+ {
+ capability: null,
+ text: 'external link text',
+ url: 'http://external/link/url',
+ },
+ ]);
+ return element.reload().then(() => {
+ assert.equal(element.filteredLinks!.length, 3);
+ assert.deepEqual(element.filteredLinks![1], {
+ capability: undefined,
+ url: '/internal/link/url',
+ name: 'internal link text',
+ noBaseUrl: true,
+ view: undefined,
+ viewableToAll: true,
+ target: null,
+ });
+ assert.deepEqual(element.filteredLinks![2], {
+ capability: undefined,
+ url: 'http://external/link/url',
+ name: 'external link text',
+ noBaseUrl: false,
+ view: undefined,
+ viewableToAll: true,
+ target: '_blank',
+ });
+ });
+ });
+
+ test('Repo shows up in nav', async () => {
+ element.repoName = 'Test Repo' as RepoName;
+ stubRestApi('getAccount').returns(
+ Promise.resolve({
+ name: 'test-user',
+ registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+ })
+ );
+ stubRestApi('getAccountCapabilities').returns(
+ Promise.resolve(createAdminCapabilities())
+ );
+ await element.reload();
+ await element.updateComplete;
+ assert.equal(queryAll<HTMLLIElement>(element, '.sectionTitle').length, 3);
+ assert.equal(
+ queryAndAssert<HTMLSpanElement>(element, '.breadcrumbText').innerText,
+ 'Test Repo'
+ );
+ assert.equal(
+ queryAndAssert<GrDropdownList>(element, '#pageSelect').items!.length,
+ 7
+ );
+ });
+
+ test('Group shows up in nav', async () => {
+ element.groupId = 'a15262' as GroupId;
+ element.groupName = 'my-group' as GroupName;
+ element.groupIsInternal = true;
+ stubRestApi('getIsAdmin').returns(Promise.resolve(true));
+ stubRestApi('getAccount').returns(
+ Promise.resolve({
+ name: 'test-user',
+ registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+ })
+ );
+ stubRestApi('getAccountCapabilities').returns(
+ Promise.resolve(createAdminCapabilities())
+ );
+ await element.reload();
+ await element.updateComplete;
+ assert.equal(element.filteredLinks!.length, 3);
+ // Repos
+ assert.isNotOk(element.filteredLinks![0].subsection);
+ // Groups
+ assert.equal(element.filteredLinks![1].subsection!.children!.length, 2);
+ assert.equal(element.filteredLinks![1].subsection!.name, 'my-group');
+ // Plugins
+ assert.isNotOk(element.filteredLinks![2].subsection);
+ });
+
+ test('Nav is reloaded when repo changes', async () => {
+ stubRestApi('getAccountCapabilities').returns(
+ Promise.resolve(createAdminCapabilities())
+ );
+ stubRestApi('getAccount').returns(
+ Promise.resolve({
+ _id: 1,
+ registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+ })
+ );
+ const reloadStub = sinon.stub(element, 'reload');
+ element.params = {repo: 'Test Repo' as RepoName, view: GerritView.REPO};
+ await element.updateComplete;
+ assert.equal(reloadStub.callCount, 1);
+ element.params = {repo: 'Test Repo 2' as RepoName, view: GerritView.REPO};
+ await element.updateComplete;
+ assert.equal(reloadStub.callCount, 2);
+ });
+
+ test('Nav is reloaded when group changes', async () => {
+ sinon.stub(element, 'computeGroupName');
+ stubRestApi('getAccountCapabilities').returns(
+ Promise.resolve(createAdminCapabilities())
+ );
+ stubRestApi('getAccount').returns(
+ Promise.resolve({
+ _id: 1,
+ registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+ })
+ );
+ const reloadStub = sinon.stub(element, 'reload');
+ element.params = {groupId: '1' as GroupId, view: GerritView.GROUP};
+ await element.updateComplete;
+ assert.equal(reloadStub.callCount, 1);
+ });
+
+ test('Nav is reloaded when group name changes', async () => {
+ const newName = 'newName' as GroupName;
+ const reloadCalled = mockPromise();
+ sinon.stub(element, 'computeGroupName');
+ sinon.stub(element, 'reload').callsFake(() => {
+ reloadCalled.resolve();
+ return Promise.resolve();
+ });
+ element.params = {groupId: '1' as GroupId, view: GerritView.GROUP};
+ element.groupName = 'oldName' as GroupName;
+ await element.updateComplete;
+ queryAndAssert<GrGroup>(element, 'gr-group').dispatchEvent(
+ new CustomEvent('name-changed', {
+ detail: {name: newName},
+ composed: true,
+ bubbles: true,
+ })
+ );
+ await reloadCalled;
+ assert.equal(element.groupName, newName);
+ });
+
+ test('dropdown displays if there is a subsection', async () => {
+ assert.isNotOk(query(element, '.mainHeader'));
+ element.subsectionLinks = [
+ {
+ text: 'Home',
+ value: 'repo',
+ view: GerritView.REPO,
+ parent: 'my-repo' as RepoName,
+ detailType: undefined,
+ },
+ ];
+ await element.updateComplete;
+ assert.isOk(query(element, '.mainHeader'));
+ element.subsectionLinks = undefined;
+ await element.updateComplete;
+ assert.isNotOk(query(element, '.mainHeader'));
+ });
+
+ test('Dropdown only triggers navigation on explicit select', async () => {
+ element.repoName = 'my-repo' as RepoName;
+ element.params = {
+ repo: 'my-repo' as RepoName,
+ view: GerritNav.View.REPO,
+ detail: GerritNav.RepoDetailView.ACCESS,
+ };
+ stubRestApi('getAccountCapabilities').returns(
+ Promise.resolve(createAdminCapabilities())
+ );
+ stubRestApi('getAccount').returns(
+ Promise.resolve({
+ _id: 1,
+ registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+ })
+ );
+ await element.updateComplete;
+ const expectedFilteredLinks = [
+ {
+ name: 'Repositories',
+ noBaseUrl: true,
+ url: '/admin/repos',
+ view: 'gr-repo-list' as GerritView,
+ viewableToAll: true,
+ subsection: {
+ name: 'my-repo',
+ view: GerritView.REPO,
+ children: [
+ {
+ name: 'General',
+ view: GerritView.REPO,
+ url: '',
+ detailType: GerritNav.RepoDetailView.GENERAL,
+ },
+ {
+ name: 'Access',
+ view: GerritView.REPO,
+ detailType: GerritNav.RepoDetailView.ACCESS,
+ url: '',
+ },
+ {
+ name: 'Commands',
+ view: GerritView.REPO,
+ detailType: GerritNav.RepoDetailView.COMMANDS,
+ url: '',
+ },
+ {
+ name: 'Branches',
+ view: GerritView.REPO,
+ detailType: GerritNav.RepoDetailView.BRANCHES,
+ url: '',
+ },
+ {
+ name: 'Tags',
+ view: GerritView.REPO,
+ detailType: GerritNav.RepoDetailView.TAGS,
+ url: '',
+ },
+ {
+ name: 'Dashboards',
+ view: GerritView.REPO,
+ detailType: GerritNav.RepoDetailView.DASHBOARDS,
+ url: '',
+ },
+ ],
+ },
+ },
+ {
+ name: 'Groups',
+ section: 'Groups',
+ noBaseUrl: true,
+ url: '/admin/groups',
+ view: 'gr-admin-group-list' as GerritView,
+ },
+ {
+ name: 'Plugins',
+ capability: 'viewPlugins',
+ section: 'Plugins',
+ noBaseUrl: true,
+ url: '/admin/plugins',
+ view: 'gr-plugin-list' as GerritView,
+ },
+ ];
+ const expectedSubsectionLinks = [
+ {
+ text: 'Home',
+ value: 'repo',
+ view: GerritView.REPO,
+ url: undefined,
+ parent: 'my-repo' as RepoName,
+ detailType: undefined,
+ },
+ {
+ text: 'General',
+ value: 'repogeneral',
+ view: GerritView.REPO,
+ url: '',
+ detailType: GerritNav.RepoDetailView.GENERAL,
+ parent: 'my-repo' as RepoName,
+ },
+ {
+ text: 'Access',
+ value: 'repoaccess',
+ view: GerritView.REPO,
+ url: '',
+ detailType: GerritNav.RepoDetailView.ACCESS,
+ parent: 'my-repo' as RepoName,
+ },
+ {
+ text: 'Commands',
+ value: 'repocommands',
+ view: GerritView.REPO,
+ url: '',
+ detailType: GerritNav.RepoDetailView.COMMANDS,
+ parent: 'my-repo' as RepoName,
+ },
+ {
+ text: 'Branches',
+ value: 'repobranches',
+ view: GerritView.REPO,
+ url: '',
+ detailType: GerritNav.RepoDetailView.BRANCHES,
+ parent: 'my-repo' as RepoName,
+ },
+ {
+ text: 'Tags',
+ value: 'repotags',
+ view: GerritView.REPO,
+ url: '',
+ detailType: GerritNav.RepoDetailView.TAGS,
+ parent: 'my-repo' as RepoName,
+ },
+ {
+ text: 'Dashboards',
+ value: 'repodashboards',
+ view: GerritView.REPO,
+ url: '',
+ detailType: GerritNav.RepoDetailView.DASHBOARDS,
+ parent: 'my-repo' as RepoName,
+ },
+ ];
+ const navigateToRelativeUrlStub = sinon.stub(
+ GerritNav,
+ 'navigateToRelativeUrl'
+ );
+ const selectedIsCurrentPageSpy = sinon.spy(
+ element,
+ 'selectedIsCurrentPage'
+ );
+ sinon.spy(element, 'handleSubsectionChange');
+ await element.reload();
+ assert.deepEqual(element.filteredLinks, expectedFilteredLinks);
+ assert.deepEqual(element.subsectionLinks, expectedSubsectionLinks);
+ assert.equal(
+ queryAndAssert<GrDropdownList>(element, '#pageSelect').value,
+ 'repoaccess'
+ );
+ assert.isTrue(selectedIsCurrentPageSpy.calledOnce);
+ // Doesn't trigger navigation from the page select menu.
+ assert.isFalse(navigateToRelativeUrlStub.called);
+
+ // When explicitly changed, navigation is called
+ queryAndAssert<GrDropdownList>(element, '#pageSelect').value =
+ 'repogeneral';
+ assert.isTrue(selectedIsCurrentPageSpy.calledTwice);
+ assert.isTrue(navigateToRelativeUrlStub.calledOnce);
+ });
+
+ test('selectedIsCurrentPage', () => {
+ element.repoName = 'my-repo' as RepoName;
+ element.params = {view: GerritView.REPO, repo: 'my-repo' as RepoName};
+ const selected = {
+ view: GerritView.REPO,
+ parent: 'my-repo' as RepoName,
+ value: '',
+ text: '',
+ } as AdminSubsectionLink;
+ assert.isTrue(element.selectedIsCurrentPage(selected));
+ selected.parent = 'my-second-repo' as RepoName;
+ assert.isFalse(element.selectedIsCurrentPage(selected));
+ selected.detailType = GerritNav.RepoDetailView.GENERAL;
+ assert.isFalse(element.selectedIsCurrentPage(selected));
+ });
+
+ suite('computeSelectedClass', () => {
+ setup(async () => {
+ stubRestApi('getAccountCapabilities').returns(
+ Promise.resolve(createAdminCapabilities())
+ );
+ stubRestApi('getAccount').returns(
+ Promise.resolve({
+ _id: 1,
+ registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+ })
+ );
+ await element.reload();
+ });
+
+ suite('repos', () => {
+ setup(() => {
+ stub('gr-repo-access', '_repoChanged').callsFake(() =>
+ Promise.resolve()
+ );
+ });
+
+ test('repo list', async () => {
+ element.params = {
+ view: GerritNav.View.ADMIN,
+ adminView: 'gr-repo-list',
+ openCreateModal: false,
+ };
+ await element.updateComplete;
+ const selected = queryAndAssert(element, 'gr-page-nav .selected');
+ assert.isOk(selected);
+ assert.equal(selected.textContent!.trim(), 'Repositories');
+ });
+
+ test('repo', async () => {
+ element.params = {
+ view: GerritNav.View.REPO,
+ repo: 'foo' as RepoName,
+ };
+ element.repoName = 'foo' as RepoName;
+ await element.reload();
+ await element.updateComplete;
+ const selected = queryAndAssert(element, 'gr-page-nav .selected');
+ assert.isOk(selected);
+ assert.equal(selected.textContent!.trim(), 'foo');
+ });
+
+ test('repo access', async () => {
+ element.params = {
+ view: GerritNav.View.REPO,
+ detail: GerritNav.RepoDetailView.ACCESS,
+ repo: 'foo' as RepoName,
+ };
+ element.repoName = 'foo' as RepoName;
+ await element.reload();
+ await element.updateComplete;
+ const selected = queryAndAssert(element, 'gr-page-nav .selected');
+ assert.isOk(selected);
+ assert.equal(selected.textContent!.trim(), 'Access');
+ });
+
+ test('repo dashboards', async () => {
+ element.params = {
+ view: GerritNav.View.REPO,
+ detail: GerritNav.RepoDetailView.DASHBOARDS,
+ repo: 'foo' as RepoName,
+ };
+ element.repoName = 'foo' as RepoName;
+ await element.reload();
+ await element.updateComplete;
+ const selected = queryAndAssert(element, 'gr-page-nav .selected');
+ assert.isOk(selected);
+ assert.equal(selected.textContent!.trim(), 'Dashboards');
+ });
+ });
+
+ suite('groups', () => {
+ let getGroupConfigStub: sinon.SinonStub;
+
+ setup(async () => {
+ stub('gr-group', 'loadGroup').callsFake(() => Promise.resolve());
+ stub('gr-group-members', 'loadGroupDetails').callsFake(() =>
+ Promise.resolve()
+ );
+
+ getGroupConfigStub = stubRestApi('getGroupConfig');
+ getGroupConfigStub.returns(
+ Promise.resolve({
+ name: 'foo',
+ id: 'c0f83e941ce90caea30e6ad88f0d4ea0e841a7a9',
+ })
+ );
+ stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
+ await element.reload();
+ });
+
+ test('group list', async () => {
+ element.params = {
+ view: GerritNav.View.ADMIN,
+ adminView: 'gr-admin-group-list',
+ openCreateModal: false,
+ };
+ await element.updateComplete;
+ const selected = queryAndAssert(element, 'gr-page-nav .selected');
+ assert.isOk(selected);
+ assert.equal(selected.textContent!.trim(), 'Groups');
+ });
+
+ test('internal group', async () => {
+ element.params = {
+ view: GerritNav.View.GROUP,
+ groupId: '1234' as GroupId,
+ };
+ element.groupName = 'foo' as GroupName;
+ await element.reload();
+ await element.updateComplete;
+ const subsectionItems = queryAll<HTMLLIElement>(
+ element,
+ '.subsectionItem'
+ );
+ assert.equal(subsectionItems.length, 2);
+ assert.isTrue(element.groupIsInternal);
+ const selected = queryAndAssert(element, 'gr-page-nav .selected');
+ assert.isOk(selected);
+ assert.equal(selected.textContent!.trim(), 'foo');
+ });
+
+ test('external group', async () => {
+ getGroupConfigStub.returns(
+ Promise.resolve({
+ name: 'foo',
+ id: 'external-id',
+ })
+ );
+ element.params = {
+ view: GerritNav.View.GROUP,
+ groupId: '1234' as GroupId,
+ };
+ element.groupName = 'foo' as GroupName;
+ await element.reload();
+ await element.updateComplete;
+ const subsectionItems = queryAll<HTMLLIElement>(
+ element,
+ '.subsectionItem'
+ );
+ assert.equal(subsectionItems.length, 0);
+ assert.isFalse(element.groupIsInternal);
+ const selected = queryAndAssert(element, 'gr-page-nav .selected');
+ assert.isOk(selected);
+ assert.equal(selected.textContent!.trim(), 'foo');
+ });
+
+ test('group members', async () => {
+ element.params = {
+ view: GerritNav.View.GROUP,
+ detail: GerritNav.GroupDetailView.MEMBERS,
+ groupId: '1234' as GroupId,
+ };
+ element.groupName = 'foo' as GroupName;
+ await element.reload();
+ await element.updateComplete;
+ const selected = queryAndAssert(element, 'gr-page-nav .selected');
+ assert.isOk(selected);
+ assert.equal(selected.textContent!.trim(), 'Members');
+ });
+ });
+ });
+});
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
index 15f6f4b..119b905 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
@@ -20,121 +20,207 @@
import '../../shared/gr-autocomplete/gr-autocomplete';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-select/gr-select';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-create-change-dialog_html';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {customElement, property, observe} from '@polymer/decorators';
import {
RepoName,
BranchName,
ChangeId,
- ConfigInfo,
InheritedBooleanInfo,
} from '../../../types/common';
import {InheritedBooleanInfoConfiguredValue} from '../../../constants/constants';
-import {GrTypedAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
-import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
-import {appContext} from '../../../services/app-context';
-import {Subject} from 'rxjs';
-import {
- repoConfig$,
- serverConfig$,
-} from '../../../services/config/config-model';
-import {takeUntil} from 'rxjs/operators';
-import {IronInputElement} from '@polymer/iron-input/iron-input';
+import {getAppContext} from '../../../services/app-context';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, css, html} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
+import {BindValueChangeEvent} from '../../../types/events';
+import {fireEvent} from '../../../utils/event-util';
+import {subscribe} from '../../lit/subscription-controller';
+import {configModelToken} from '../../../models/config/config-model';
+import {resolve} from '../../../models/dependency';
const SUGGESTIONS_LIMIT = 15;
const REF_PREFIX = 'refs/heads/';
-export interface GrCreateChangeDialog {
- $: {
- privateChangeCheckBox: HTMLInputElement;
- branchInput: GrTypedAutocomplete<BranchName>;
- tagNameInput: IronInputElement;
- messageInput: IronAutogrowTextareaElement;
- };
-}
-@customElement('gr-create-change-dialog')
-export class GrCreateChangeDialog extends PolymerElement {
- static get template() {
- return htmlTemplate;
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-create-change-dialog': GrCreateChangeDialog;
}
+}
+
+@customElement('gr-create-change-dialog')
+export class GrCreateChangeDialog extends LitElement {
+ // private but used in test
+ @query('#privateChangeCheckBox') privateChangeCheckBox!: HTMLInputElement;
@property({type: String})
repoName?: RepoName;
- @property({type: String})
- branch = '' as BranchName;
+ // private but used in test
+ @state() branch = '' as BranchName;
- @property({type: Object})
- _repoConfig?: ConfigInfo;
+ // private but used in test
+ @state() subject = '';
- @property({type: String})
- subject = '';
+ // private but used in test
+ @state() topic?: string;
- @property({type: String})
- topic?: string;
+ @state() private baseChange?: ChangeId;
- @property({type: Object})
- _query?: (input: string) => Promise<{name: BranchName}[]>;
-
- @property({type: String})
- baseChange?: ChangeId;
-
- @property({type: String})
- baseCommit?: string;
+ @state() private baseCommit?: string;
@property({type: Object})
privateByDefault?: InheritedBooleanInfo;
- @property({type: Boolean, notify: true})
- canCreate = false;
+ @state() private privateChangesEnabled = false;
- @property({type: Boolean})
- _privateChangesEnabled = false;
+ private readonly query: (input: string) => Promise<{name: BranchName}[]>;
- restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
- disconnected$ = new Subject();
+ private readonly configModel = resolve(this, configModelToken);
constructor() {
super();
- this._query = (input: string) => this._getRepoBranchesSuggestions(input);
+ this.query = (input: string) => this.getRepoBranchesSuggestions(input);
}
override connectedCallback() {
super.connectedCallback();
if (!this.repoName) return;
- repoConfig$.pipe(takeUntil(this.disconnected$)).subscribe(config => {
- this.privateByDefault = config?.private_by_default;
- });
-
- serverConfig$.pipe(takeUntil(this.disconnected$)).subscribe(config => {
- this._privateChangesEnabled =
+ subscribe(this, this.configModel().serverConfig$, config => {
+ this.privateChangesEnabled =
config?.change?.disable_private_changes ?? false;
});
}
- override disconnectedCallback() {
- this.disconnected$.next();
- super.disconnectedCallback();
+ static override get styles() {
+ return [
+ formStyles,
+ sharedStyles,
+ css`
+ input:not([type='checkbox']),
+ gr-autocomplete,
+ iron-autogrow-textarea {
+ width: 100%;
+ }
+ .value {
+ width: 32em;
+ }
+ .hide {
+ display: none;
+ }
+ @media only screen and (max-width: 40em) {
+ .value {
+ width: 29em;
+ }
+ }
+ `,
+ ];
}
- _computeBranchClass(baseChange?: ChangeId) {
- return baseChange ? 'hide' : '';
+ override render() {
+ return html`
+ <div class="gr-form-styles">
+ <section class=${this.baseChange ? 'hide' : ''}>
+ <span class="title">Select branch for new change</span>
+ <span class="value">
+ <gr-autocomplete
+ id="branchInput"
+ .text=${this.branch}
+ .query=${this.query}
+ placeholder="Destination branch"
+ @text-changed=${(e: CustomEvent) => {
+ this.branch = e.detail.value;
+ }}
+ >
+ </gr-autocomplete>
+ </span>
+ </section>
+ <section class=${this.baseChange ? 'hide' : ''}>
+ <span class="title">Provide base commit sha1 for change</span>
+ <span class="value">
+ <iron-input
+ .bindValue=${this.baseCommit}
+ @bind-value-changed=${(e: BindValueChangeEvent) => {
+ this.baseCommit = e.detail.value;
+ }}
+ >
+ <input
+ id="baseCommitInput"
+ maxlength="40"
+ placeholder="(optional)"
+ />
+ </iron-input>
+ </span>
+ </section>
+ <section>
+ <span class="title">Enter topic for new change</span>
+ <span class="value">
+ <iron-input
+ .bindValue=${this.topic}
+ @bind-value-changed=${(e: BindValueChangeEvent) => {
+ this.topic = e.detail.value;
+ }}
+ >
+ <input
+ id="tagNameInput"
+ maxlength="1024"
+ placeholder="(optional)"
+ />
+ </iron-input>
+ </span>
+ </section>
+ <section id="description">
+ <span class="title">Description</span>
+ <span class="value">
+ <iron-autogrow-textarea
+ id="messageInput"
+ class="message"
+ autocomplete="on"
+ rows="4"
+ maxRows="15"
+ .bindValue=${this.subject}
+ placeholder="Insert the description of the change."
+ @bind-value-changed=${(e: BindValueChangeEvent) => {
+ this.subject = e.detail.value;
+ }}
+ >
+ </iron-autogrow-textarea>
+ </span>
+ </section>
+ <section class=${this.privateChangesEnabled ? 'hide' : ''}>
+ <label class="title" for="privateChangeCheckBox"
+ >Private change</label
+ >
+ <span class="value">
+ <input
+ type="checkbox"
+ id="privateChangeCheckBox"
+ ?checked=${this.formatPrivateByDefaultBoolean()}
+ />
+ </span>
+ </section>
+ </div>
+ `;
}
- @observe('branch', 'subject')
- _allowCreate(branch: BranchName, subject: string) {
- this.canCreate = !!branch && !!subject;
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('branch') || changedProperties.has('subject')) {
+ this.allowCreate();
+ }
+ }
+
+ private allowCreate() {
+ fireEvent(this, 'can-create-change');
}
handleCreateChange(): Promise<void> {
if (!this.repoName || !this.branch || !this.subject) {
return Promise.resolve();
}
- const isPrivate = this.$.privateChangeCheckBox.checked;
+ const isPrivate = this.privateChangeCheckBox.checked;
const isWip = true;
return this.restApiService
.createChange(
@@ -148,14 +234,13 @@
this.baseCommit || undefined
)
.then(changeCreated => {
- if (!changeCreated) {
- return;
- }
+ if (!changeCreated) return;
GerritNav.navigateToChange(changeCreated);
});
}
- _getRepoBranchesSuggestions(input: string) {
+ // private but used in test
+ getRepoBranchesSuggestions(input: string) {
if (!this.repoName) {
return Promise.reject(new Error('missing repo name'));
}
@@ -178,34 +263,19 @@
});
}
- _formatBooleanString(config?: InheritedBooleanInfo) {
- if (
- config &&
- config.configured_value === InheritedBooleanInfoConfiguredValue.TRUE
- ) {
- return true;
- } else if (
- config &&
- config.configured_value === InheritedBooleanInfoConfiguredValue.FALSE
- ) {
- return false;
- } else if (
- config &&
- config.configured_value === InheritedBooleanInfoConfiguredValue.INHERITED
- ) {
- return !!(config && config.inherited_value);
- } else {
- return false;
+ // private but used in test
+ formatPrivateByDefaultBoolean() {
+ const config = this.privateByDefault;
+ if (config === undefined) return false;
+ switch (config.configured_value) {
+ case InheritedBooleanInfoConfiguredValue.TRUE:
+ return true;
+ case InheritedBooleanInfoConfiguredValue.FALSE:
+ return false;
+ case InheritedBooleanInfoConfiguredValue.INHERITED:
+ return !!config.inherited_value;
+ default:
+ return false;
}
}
-
- _computePrivateSectionClass(config: boolean) {
- return config ? 'hide' : '';
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-create-change-dialog': GrCreateChangeDialog;
- }
}
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.ts
deleted file mode 100644
index 47f3818..0000000
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-form-styles">
- input:not([type='checkbox']),
- gr-autocomplete,
- iron-autogrow-textarea {
- width: 100%;
- }
- .value {
- width: 32em;
- }
- .hide {
- display: none;
- }
- @media only screen and (max-width: 40em) {
- .value {
- width: 29em;
- }
- }
- </style>
- <div class="gr-form-styles">
- <section class$="[[_computeBranchClass(baseChange)]]">
- <span class="title">Select branch for new change</span>
- <span class="value">
- <gr-autocomplete
- id="branchInput"
- text="{{branch}}"
- query="[[_query]]"
- placeholder="Destination branch"
- >
- </gr-autocomplete>
- </span>
- </section>
- <section class$="[[_computeBranchClass(baseChange)]]">
- <span class="title">Provide base commit sha1 for change</span>
- <span class="value">
- <iron-input
- maxlength="40"
- placeholder="(optional)"
- bind-value="{{baseCommit}}"
- >
- <input
- is="iron-input"
- id="baseCommitInput"
- maxlength="40"
- placeholder="(optional)"
- bind-value="{{baseCommit}}"
- />
- </iron-input>
- </span>
- </section>
- <section>
- <span class="title">Enter topic for new change</span>
- <span class="value">
- <iron-input
- maxlength="1024"
- placeholder="(optional)"
- bind-value="{{topic}}"
- >
- <input
- is="iron-input"
- id="tagNameInput"
- maxlength="1024"
- placeholder="(optional)"
- bind-value="{{topic}}"
- />
- </iron-input>
- </span>
- </section>
- <section id="description">
- <span class="title">Description</span>
- <span class="value">
- <iron-autogrow-textarea
- id="messageInput"
- class="message"
- autocomplete="on"
- rows="4"
- max-rows="15"
- bind-value="{{subject}}"
- placeholder="Insert the description of the change."
- >
- </iron-autogrow-textarea>
- </span>
- </section>
- <section class$="[[_computePrivateSectionClass(_privateChangesEnabled)]]">
- <label class="title" for="privateChangeCheckBox">Private change</label>
- <span class="value">
- <input
- type="checkbox"
- id="privateChangeCheckBox"
- checked$="[[_formatBooleanString(privateByDefault)]]"
- />
- </span>
- </section>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
index 9ed5d81..626b03f 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
@@ -20,19 +20,16 @@
import {GrCreateChangeDialog} from './gr-create-change-dialog';
import {BranchName, GitRef, RepoName} from '../../../types/common';
import {InheritedBooleanInfoConfiguredValue} from '../../../constants/constants';
-import {
- createChange,
- createConfig,
- TEST_CHANGE_ID,
-} from '../../../test/test-data-generators';
-import {stubRestApi} from '../../../test/test-utils';
+import {createChange} from '../../../test/test-data-generators';
+import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
+import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
const basicFixture = fixtureFromElement('gr-create-change-dialog');
suite('gr-create-change-dialog tests', () => {
let element: GrCreateChangeDialog;
- setup(() => {
+ setup(async () => {
stubRestApi('getRepoBranches').callsFake((input: string) => {
if (input.startsWith('test')) {
return Promise.resolve([
@@ -47,15 +44,8 @@
}
});
element = basicFixture.instantiate();
+ await element.updateComplete;
element.repoName = 'test-repo' as RepoName;
- element._repoConfig = {
- ...createConfig(),
- private_by_default: {
- value: false,
- configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
- inherited_value: false,
- },
- };
});
test('new change created with default', async () => {
@@ -74,9 +64,13 @@
element.branch = 'test-branch' as BranchName;
element.topic = 'test-topic';
element.subject = 'first change created with polygerrit ui';
- assert.isFalse(element.$.privateChangeCheckBox.checked);
+ assert.isFalse(element.privateChangeCheckBox.checked);
- element.$.messageInput.bindValue = configInputObj.subject;
+ const messageInput = queryAndAssert<IronAutogrowTextareaElement>(
+ element,
+ '#messageInput'
+ );
+ messageInput.bindValue = configInputObj.subject;
await element.handleCreateChange();
// Private change
@@ -92,8 +86,8 @@
inherited_value: false,
value: true,
};
- sinon.stub(element, '_formatBooleanString').callsFake(() => true);
- flush();
+ sinon.stub(element, 'formatPrivateByDefaultBoolean').callsFake(() => true);
+ await element.updateComplete;
const configInputObj = {
branch: 'test-branch',
@@ -110,9 +104,13 @@
element.branch = 'test-branch' as BranchName;
element.topic = 'test-topic';
element.subject = 'first change created with polygerrit ui';
- assert.isTrue(element.$.privateChangeCheckBox.checked);
+ assert.isTrue(element.privateChangeCheckBox.checked);
- element.$.messageInput.bindValue = configInputObj.subject;
+ const messageInput = queryAndAssert<IronAutogrowTextareaElement>(
+ element,
+ '#messageInput'
+ );
+ messageInput.bindValue = configInputObj.subject;
await element.handleCreateChange();
// Private change
@@ -122,24 +120,14 @@
assert.isTrue(saveStub.called);
});
- test('_getRepoBranchesSuggestions empty', async () => {
- const branches = await element._getRepoBranchesSuggestions('nonexistent');
+ test('getRepoBranchesSuggestions empty', async () => {
+ const branches = await element.getRepoBranchesSuggestions('nonexistent');
assert.equal(branches.length, 0);
});
- test('_getRepoBranchesSuggestions non-empty', async () => {
- const branches = await element._getRepoBranchesSuggestions('test-branch');
+ test('getRepoBranchesSuggestions non-empty', async () => {
+ const branches = await element.getRepoBranchesSuggestions('test-branch');
assert.equal(branches.length, 1);
assert.equal(branches[0].name, 'test-branch');
});
-
- test('_computeBranchClass', () => {
- assert.equal(element._computeBranchClass(TEST_CHANGE_ID), 'hide');
- assert.equal(element._computeBranchClass(undefined), '');
- });
-
- test('_computePrivateSectionClass', () => {
- assert.equal(element._computePrivateSectionClass(true), 'hide');
- assert.equal(element._computePrivateSectionClass(false), '');
- });
});
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.ts
index 180e60a..9ab7646 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.ts
@@ -17,61 +17,95 @@
import '@polymer/iron-input/iron-input';
import '../../../styles/gr-form-styles';
import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-create-group-dialog_html';
import {encodeURL, getBaseUrl} from '../../../utils/url-util';
import {page} from '../../../utils/page-wrapper-utils';
-import {customElement, property, observe} from '@polymer/decorators';
import {GroupName} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
-
-@customElement('gr-create-group-dialog')
-export class GrCreateGroupDialog extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
- @property({type: Boolean, notify: true})
- hasNewGroupName = false;
-
- @property({type: String})
- _name: GroupName | '' = '';
-
- @property({type: Boolean})
- _groupCreated = false;
-
- private readonly restApiService = appContext.restApiService;
-
- _computeGroupUrl(groupId: string) {
- return getBaseUrl() + '/admin/groups/' + encodeURL(groupId, true);
- }
-
- @observe('_name')
- _updateGroupName(name: string) {
- this.hasNewGroupName = !!name;
- }
-
- override focus() {
- this.shadowRoot?.querySelector('input')?.focus();
- }
-
- handleCreateGroup() {
- const name = this._name as GroupName;
- return this.restApiService.createGroup({name}).then(groupRegistered => {
- if (groupRegistered.status !== 201) {
- return;
- }
- this._groupCreated = true;
- return this.restApiService.getGroupConfig(name).then(group => {
- // TODO(TS): should group always defined ?
- page.show(this._computeGroupUrl(String(group!.group_id!)));
- });
- });
- }
-}
+import {getAppContext} from '../../../services/app-context';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, css, html} from 'lit';
+import {customElement, query, property} from 'lit/decorators';
+import {BindValueChangeEvent} from '../../../types/events';
+import {fireEvent} from '../../../utils/event-util';
declare global {
interface HTMLElementTagNameMap {
'gr-create-group-dialog': GrCreateGroupDialog;
}
}
+
+@customElement('gr-create-group-dialog')
+export class GrCreateGroupDialog extends LitElement {
+ @query('input') private input!: HTMLInputElement;
+
+ @property({type: String})
+ name: GroupName | '' = '';
+
+ private readonly restApiService = getAppContext().restApiService;
+
+ static override get styles() {
+ return [
+ formStyles,
+ sharedStyles,
+ css`
+ :host {
+ display: inline-block;
+ }
+ input {
+ width: 20em;
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`
+ <div class="gr-form-styles">
+ <div id="form">
+ <section>
+ <span class="title">Group name</span>
+ <iron-input
+ .bindValue=${this.name}
+ @bind-value-changed=${this.handleGroupNameBindValueChanged}
+ >
+ <input />
+ </iron-input>
+ </section>
+ </div>
+ </div>
+ `;
+ }
+
+ override updated(changedProperties: PropertyValues) {
+ if (changedProperties.has('name')) {
+ this.updateGroupName();
+ }
+ }
+
+ private updateGroupName() {
+ fireEvent(this, 'has-new-group-name');
+ }
+
+ private computeGroupUrl(groupId: string) {
+ return getBaseUrl() + '/admin/groups/' + encodeURL(groupId, true);
+ }
+
+ override focus() {
+ this.input.focus();
+ }
+
+ handleCreateGroup() {
+ const name = this.name as GroupName;
+ return this.restApiService.createGroup({name}).then(groupRegistered => {
+ if (groupRegistered.status !== 201) return;
+ return this.restApiService.getGroupConfig(name).then(group => {
+ if (!group) return;
+ page.show(this.computeGroupUrl(String(group.group_id!)));
+ });
+ });
+ }
+
+ private handleGroupNameBindValueChanged(e: BindValueChangeEvent) {
+ this.name = e.detail.value as GroupName;
+ }
+}
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.ts b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.ts
deleted file mode 100644
index daf8780..0000000
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-form-styles">
- :host {
- display: inline-block;
- }
- input {
- width: 20em;
- }
- </style>
- <div class="gr-form-styles">
- <div id="form">
- <section>
- <span class="title">Group name</span>
- <iron-input bind-value="{{_name}}">
- <input is="iron-input" bind-value="{{_name}}" />
- </iron-input>
- </section>
- </div>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.js b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.js
deleted file mode 100644
index 321f069..0000000
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-create-group-dialog.js';
-import {page} from '../../../utils/page-wrapper-utils.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-create-group-dialog');
-
-suite('gr-create-group-dialog tests', () => {
- let element;
-
- const GROUP_NAME = 'test-group';
-
- setup(() => {
- element = basicFixture.instantiate();
- });
-
- test('name is updated correctly', async () => {
- assert.isFalse(element.hasNewGroupName);
-
- const inputEl = element.root.querySelector('iron-input');
- inputEl.bindValue = GROUP_NAME;
-
- await new Promise(resolve => setTimeout(resolve));
- assert.isTrue(element.hasNewGroupName);
- assert.deepEqual(element._name, GROUP_NAME);
- });
-
- test('test for redirecting to group on successful creation', async () => {
- stubRestApi('createGroup').returns(Promise.resolve({status: 201}));
- stubRestApi('getGroupConfig').returns(Promise.resolve({group_id: 551}));
-
- const showStub = sinon.stub(page, 'show');
- await element.handleCreateGroup();
- assert.isTrue(showStub.calledWith('/admin/groups/551'));
- });
-
- test('test for unsuccessful group creation', async () => {
- stubRestApi('createGroup').returns(Promise.resolve({status: 409}));
- stubRestApi('getGroupConfig').returns(Promise.resolve({group_id: 551}));
-
- const showStub = sinon.stub(page, 'show');
- await element.handleCreateGroup();
- assert.isFalse(showStub.called);
- });
-});
-
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.ts b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.ts
new file mode 100644
index 0000000..f84c76c
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.ts
@@ -0,0 +1,82 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-create-group-dialog';
+import {GrCreateGroupDialog} from './gr-create-group-dialog';
+import {page} from '../../../utils/page-wrapper-utils';
+import {
+ mockPromise,
+ queryAndAssert,
+ stubRestApi,
+} from '../../../test/test-utils';
+import {IronInputElement} from '@polymer/iron-input';
+import {GroupId} from '../../../types/common';
+
+const basicFixture = fixtureFromElement('gr-create-group-dialog');
+
+suite('gr-create-group-dialog tests', () => {
+ let element: GrCreateGroupDialog;
+
+ const GROUP_NAME = 'test-group';
+
+ setup(async () => {
+ element = basicFixture.instantiate();
+ await element.updateComplete;
+ });
+
+ test('name is updated correctly', async () => {
+ const promise = mockPromise();
+ element.addEventListener('has-new-group-name', () => {
+ promise.resolve();
+ });
+
+ const inputEl = queryAndAssert<IronInputElement>(element, 'iron-input');
+ inputEl.bindValue = GROUP_NAME;
+ inputEl.dispatchEvent(new Event('input', {bubbles: true, composed: true}));
+
+ await promise;
+
+ assert.deepEqual(element.name, GROUP_NAME);
+ });
+
+ test('test for redirecting to group on successful creation', async () => {
+ stubRestApi('createGroup').returns(
+ Promise.resolve({status: 201} as Response)
+ );
+ stubRestApi('getGroupConfig').returns(
+ Promise.resolve({id: 'testId551' as GroupId, group_id: 551})
+ );
+
+ const showStub = sinon.stub(page, 'show');
+ await element.handleCreateGroup();
+ assert.isTrue(showStub.calledWith('/admin/groups/551'));
+ });
+
+ test('test for unsuccessful group creation', async () => {
+ stubRestApi('createGroup').returns(
+ Promise.resolve({status: 409} as Response)
+ );
+ stubRestApi('getGroupConfig').returns(
+ Promise.resolve({id: 'testId551' as GroupId, group_id: 551})
+ );
+
+ const showStub = sinon.stub(page, 'show');
+ await element.handleCreateGroup();
+ assert.isFalse(showStub.called);
+ });
+});
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.ts
index 7fce8e5..63b852c 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.ts
@@ -15,65 +15,132 @@
* limitations under the License.
*/
import '@polymer/iron-input/iron-input';
-import '../../../styles/gr-form-styles';
-import '../../../styles/shared-styles';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-select/gr-select';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-create-pointer-dialog_html';
import {encodeURL, getBaseUrl} from '../../../utils/url-util';
import {page} from '../../../utils/page-wrapper-utils';
-import {customElement, property, observe} from '@polymer/decorators';
import {BranchName, RepoName} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {RepoDetailView} from '../../core/gr-navigation/gr-navigation';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, css, html} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+import {BindValueChangeEvent} from '../../../types/events';
+import {fireEvent} from '../../../utils/event-util';
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-create-pointer-dialog': GrCreatePointerDialog;
+ }
+}
@customElement('gr-create-pointer-dialog')
-export class GrCreatePointerDialog extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrCreatePointerDialog extends LitElement {
@property({type: String})
detailType?: string;
@property({type: String})
repoName?: RepoName;
- @property({type: Boolean, notify: true})
- hasNewItemName = false;
-
@property({type: String})
itemDetail?: RepoDetailView.BRANCHES | RepoDetailView.TAGS;
- @property({type: String})
- _itemName?: BranchName;
+ /* private but used in test */
+ @state() itemName?: BranchName;
- @property({type: String})
- _itemRevision?: string;
+ /* private but used in test */
+ @state() itemRevision?: string;
- @property({type: String})
- _itemAnnotation?: string;
+ /* private but used in test */
+ @state() itemAnnotation?: string;
- @observe('_itemName')
- _updateItemName(name?: string) {
- this.hasNewItemName = !!name;
+ private readonly restApiService = getAppContext().restApiService;
+
+ static override get styles() {
+ return [
+ formStyles,
+ sharedStyles,
+ css`
+ :host {
+ display: inline-block;
+ }
+ input {
+ width: 20em;
+ }
+ /* Add css selector with #id to increase priority
+ (otherwise ".gr-form-styles section" rule wins) */
+ .hideItem,
+ #itemAnnotationSection.hideItem {
+ display: none;
+ }
+ `,
+ ];
}
- private readonly restApiService = appContext.restApiService;
+ override render() {
+ return html`
+ <div class="gr-form-styles">
+ <div id="form">
+ <section id="itemNameSection">
+ <span class="title">${this.detailType} name</span>
+ <iron-input
+ .bindValue=${this.itemName}
+ @bind-value-changed=${this.handleItemNameBindValueChanged}
+ >
+ <input placeholder="${this.detailType} Name" />
+ </iron-input>
+ </section>
+ <section id="itemRevisionSection">
+ <span class="title">Initial Revision</span>
+ <iron-input
+ .bindValue=${this.itemRevision}
+ @bind-value-changed=${this.handleItemRevisionBindValueChanged}
+ >
+ <input placeholder="Revision (Branch or SHA-1)" />
+ </iron-input>
+ </section>
+ <section
+ id="itemAnnotationSection"
+ class=${this.itemDetail === RepoDetailView.BRANCHES
+ ? 'hideItem'
+ : ''}
+ >
+ <span class="title">Annotation</span>
+ <iron-input
+ .bindValue=${this.itemAnnotation}
+ @bind-value-changed=${this.handleItemAnnotationBindValueChanged}
+ >
+ <input placeholder="Annotation (Optional)" />
+ </iron-input>
+ </section>
+ </div>
+ </div>
+ `;
+ }
+
+ override updated(changedProperties: PropertyValues) {
+ if (changedProperties.has('itemName')) {
+ this.updateItemName();
+ }
+ }
+
+ private updateItemName() {
+ fireEvent(this, 'update-item-name');
+ }
handleCreateItem() {
if (!this.repoName) {
throw new Error('repoName name is not set');
}
- if (!this._itemName) {
+ if (!this.itemName) {
throw new Error('itemName name is not set');
}
- const USE_HEAD = this._itemRevision ? this._itemRevision : 'HEAD';
+ const USE_HEAD = this.itemRevision ? this.itemRevision : 'HEAD';
const url = `${getBaseUrl()}/admin/repos/${encodeURL(this.repoName, true)}`;
if (this.itemDetail === RepoDetailView.BRANCHES) {
return this.restApiService
- .createRepoBranch(this.repoName, this._itemName, {revision: USE_HEAD})
+ .createRepoBranch(this.repoName, this.itemName, {revision: USE_HEAD})
.then(itemRegistered => {
if (itemRegistered.status === 201) {
page.show(`${url},branches`);
@@ -81,9 +148,9 @@
});
} else if (this.itemDetail === RepoDetailView.TAGS) {
return this.restApiService
- .createRepoTag(this.repoName, this._itemName, {
+ .createRepoTag(this.repoName, this.itemName, {
revision: USE_HEAD,
- message: this._itemAnnotation || undefined,
+ message: this.itemAnnotation || undefined,
})
.then(itemRegistered => {
if (itemRegistered.status === 201) {
@@ -94,13 +161,15 @@
throw new Error(`Invalid itemDetail: ${this.itemDetail}`);
}
- _computeHideItemClass(type?: RepoDetailView.BRANCHES | RepoDetailView.TAGS) {
- return type === RepoDetailView.BRANCHES ? 'hideItem' : '';
+ private handleItemNameBindValueChanged(e: BindValueChangeEvent) {
+ this.itemName = e.detail.value as BranchName;
}
-}
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-create-pointer-dialog': GrCreatePointerDialog;
+ private handleItemRevisionBindValueChanged(e: BindValueChangeEvent) {
+ this.itemRevision = e.detail.value;
+ }
+
+ private handleItemAnnotationBindValueChanged(e: BindValueChangeEvent) {
+ this.itemAnnotation = e.detail.value;
}
}
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.ts b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.ts
deleted file mode 100644
index 0e2b157..0000000
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-form-styles">
- :host {
- display: inline-block;
- }
- input {
- width: 20em;
- }
- /* Add css selector with #id to increase priority
- (otherwise ".gr-form-styles section" rule wins) */
- .hideItem,
- #itemAnnotationSection.hideItem {
- display: none;
- }
- </style>
- <div class="gr-form-styles">
- <div id="form">
- <section id="itemNameSection">
- <span class="title">[[detailType]] name</span>
- <iron-input bind-value="{{_itemName}}">
- <input placeholder="[[detailType]] Name" />
- </iron-input>
- </section>
- <section id="itemRevisionSection">
- <span class="title">Initial Revision</span>
- <iron-input bind-value="{{_itemRevision}}">
- <input placeholder="Revision (Branch or SHA-1)" />
- </iron-input>
- </section>
- <section
- id="itemAnnotationSection"
- class$="[[_computeHideItemClass(itemDetail)]]"
- >
- <span class="title">Annotation</span>
- <iron-input bind-value="{{_itemAnnotation}}">
- <input placeholder="Annotation (Optional)" />
- </iron-input>
- </section>
- </div>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.ts b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.ts
index ea0919c..b888c348 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.ts
@@ -18,7 +18,11 @@
import '../../../test/common-test-setup-karma';
import './gr-create-pointer-dialog';
import {GrCreatePointerDialog} from './gr-create-pointer-dialog';
-import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
+import {
+ mockPromise,
+ queryAndAssert,
+ stubRestApi,
+} from '../../../test/test-utils';
import {BranchName} from '../../../types/common';
import {RepoDetailView} from '../../core/gr-navigation/gr-navigation';
import {IronInputElement} from '@polymer/iron-input';
@@ -31,73 +35,78 @@
const ironInput = (element: Element) =>
queryAndAssert<IronInputElement>(element, 'iron-input');
- setup(() => {
+ setup(async () => {
element = basicFixture.instantiate();
+ await element.updateComplete;
});
test('branch created', async () => {
stubRestApi('createRepoBranch').returns(Promise.resolve(new Response()));
- assert.isFalse(element.hasNewItemName);
+ const promise = mockPromise();
+ element.addEventListener('update-item-name', () => {
+ promise.resolve();
+ });
- element._itemName = 'test-branch' as BranchName;
+ element.itemName = 'test-branch' as BranchName;
element.itemDetail = 'branches' as RepoDetailView.BRANCHES;
- ironInput(element.$.itemNameSection).bindValue = 'test-branch2';
- ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
+ ironInput(queryAndAssert(element, '#itemNameSection')).bindValue =
+ 'test-branch2';
+ ironInput(queryAndAssert(element, '#itemRevisionSection')).bindValue =
+ 'HEAD';
- await flush();
+ await promise;
- assert.isTrue(element.hasNewItemName);
- assert.equal(element._itemName, 'test-branch2' as BranchName);
- assert.equal(element._itemRevision, 'HEAD');
+ assert.equal(element.itemName, 'test-branch2' as BranchName);
+ assert.equal(element.itemRevision, 'HEAD');
});
test('tag created', async () => {
stubRestApi('createRepoTag').returns(Promise.resolve(new Response()));
- assert.isFalse(element.hasNewItemName);
+ const promise = mockPromise();
+ element.addEventListener('update-item-name', () => {
+ promise.resolve();
+ });
- element._itemName = 'test-tag' as BranchName;
+ element.itemName = 'test-tag' as BranchName;
element.itemDetail = 'tags' as RepoDetailView.TAGS;
- ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
- ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
+ ironInput(queryAndAssert(element, '#itemNameSection')).bindValue =
+ 'test-tag2';
+ ironInput(queryAndAssert(element, '#itemRevisionSection')).bindValue =
+ 'HEAD';
- await flush();
- assert.isTrue(element.hasNewItemName);
- assert.equal(element._itemName, 'test-tag2' as BranchName);
- assert.equal(element._itemRevision, 'HEAD');
+ await promise;
+
+ assert.equal(element.itemName, 'test-tag2' as BranchName);
+ assert.equal(element.itemRevision, 'HEAD');
});
test('tag created with annotations', async () => {
stubRestApi('createRepoTag').returns(Promise.resolve(new Response()));
- assert.isFalse(element.hasNewItemName);
+ const promise = mockPromise();
+ element.addEventListener('update-item-name', () => {
+ promise.resolve();
+ });
- element._itemName = 'test-tag' as BranchName;
- element._itemAnnotation = 'test-message';
+ element.itemName = 'test-tag' as BranchName;
+ element.itemAnnotation = 'test-message';
element.itemDetail = 'tags' as RepoDetailView.TAGS;
- ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
- ironInput(element.$.itemAnnotationSection).bindValue = 'test-message2';
- ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
+ ironInput(queryAndAssert(element, '#itemNameSection')).bindValue =
+ 'test-tag2';
+ ironInput(queryAndAssert(element, '#itemAnnotationSection')).bindValue =
+ 'test-message2';
+ ironInput(queryAndAssert(element, '#itemRevisionSection')).bindValue =
+ 'HEAD';
- await flush();
- assert.isTrue(element.hasNewItemName);
- assert.equal(element._itemName, 'test-tag2' as BranchName);
- assert.equal(element._itemAnnotation, 'test-message2');
- assert.equal(element._itemRevision, 'HEAD');
- });
+ await promise;
- test('_computeHideItemClass returns hideItem if type is branches', () => {
- assert.equal(
- element._computeHideItemClass(RepoDetailView.BRANCHES),
- 'hideItem'
- );
- });
-
- test('_computeHideItemClass returns strings if not branches', () => {
- assert.equal(element._computeHideItemClass(RepoDetailView.TAGS), '');
+ assert.equal(element.itemName, 'test-tag2' as BranchName);
+ assert.equal(element.itemAnnotation, 'test-message2');
+ assert.equal(element.itemRevision, 'HEAD');
});
});
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 a493747..ea9495c 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
@@ -15,16 +15,11 @@
* limitations under the License.
*/
import '@polymer/iron-input/iron-input';
-import '../../../styles/gr-form-styles';
-import '../../../styles/shared-styles';
import '../../shared/gr-autocomplete/gr-autocomplete';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-select/gr-select';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-create-repo-dialog_html';
import {encodeURL, getBaseUrl} from '../../../utils/url-util';
import {page} from '../../../utils/page-wrapper-utils';
-import {customElement, observe, property} from '@polymer/decorators';
import {
BranchName,
GroupId,
@@ -32,55 +27,174 @@
RepoName,
} from '../../../types/common';
import {AutocompleteQuery} from '../../shared/gr-autocomplete/gr-autocomplete';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
+import {convertToString} from '../../../utils/string-util';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, css, html} from 'lit';
+import {customElement, query, property, state} from 'lit/decorators';
+import {fireEvent} from '../../../utils/event-util';
declare global {
+ interface HTMLElementEventMap {
+ 'text-changed': CustomEvent;
+ 'value-changed': CustomEvent;
+ }
interface HTMLElementTagNameMap {
'gr-create-repo-dialog': GrCreateRepoDialog;
}
}
@customElement('gr-create-repo-dialog')
-export class GrCreateRepoDialog extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
+export class GrCreateRepoDialog extends LitElement {
+ /**
+ * Fired when repostiory name is entered.
+ *
+ * @event new-repo-name
+ */
- @property({type: Boolean, notify: true})
- hasNewRepoName = false;
+ @query('input')
+ input?: HTMLInputElement;
- @property({type: Object})
- _repoConfig: ProjectInput & {name: RepoName} = {
+ @property({type: Boolean})
+ nameChanged = false;
+
+ /* private but used in test */
+ @state() repoConfig: ProjectInput & {name: RepoName} = {
create_empty_commit: true,
permissions_only: false,
name: '' as RepoName,
branches: [],
};
- @property({type: String})
- _defaultBranch?: BranchName;
+ /* private but used in test */
+ @state() defaultBranch?: BranchName;
- @property({type: Boolean})
- _repoCreated = false;
+ /* private but used in test */
+ @state() repoCreated = false;
- @property({type: String})
- _repoOwner?: string;
+ /* private but used in test */
+ @state() repoOwner?: string;
- @property({type: String})
- _repoOwnerId?: GroupId;
+ /* private but used in test */
+ @state() repoOwnerId?: GroupId;
- @property({type: Object})
- _query: AutocompleteQuery;
+ private readonly query: AutocompleteQuery;
- @property({type: Object})
- _queryGroups: AutocompleteQuery;
+ private readonly queryGroups: AutocompleteQuery;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
- this._query = (input: string) => this._getRepoSuggestions(input);
- this._queryGroups = (input: string) => this._getGroupSuggestions(input);
+ this.query = (input: string) => this.getRepoSuggestions(input);
+ this.queryGroups = (input: string) => this.getGroupSuggestions(input);
+ }
+
+ static override get styles() {
+ return [
+ formStyles,
+ sharedStyles,
+ css`
+ :host {
+ display: inline-block;
+ }
+ input {
+ width: 20em;
+ }
+ gr-autocomplete {
+ width: 20em;
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`
+ <div class="gr-form-styles">
+ <div id="form">
+ <section>
+ <span class="title">Repository name</span>
+ <iron-input
+ .bindValue=${convertToString(this.repoConfig.name)}
+ @bind-value-changed=${this.handleNameBindValueChanged}
+ >
+ <input id="repoNameInput" autocomplete="on" />
+ </iron-input>
+ </section>
+ <section>
+ <span class="title">Default Branch</span>
+ <iron-input
+ .bindValue=${convertToString(this.defaultBranch)}
+ @bind-value-changed=${this.handleBranchNameBindValueChanged}
+ >
+ <input id="defaultBranchNameInput" autocomplete="off" />
+ </iron-input>
+ </section>
+ <section>
+ <span class="title">Rights inherit from</span>
+ <span class="value">
+ <gr-autocomplete
+ id="rightsInheritFromInput"
+ .text=${convertToString(this.repoConfig.parent)}
+ .query=${this.query}
+ .placeholder=${"Optional, defaults to 'All-Projects'"}
+ @text-changed=${this.handleRightsTextChanged}
+ >
+ </gr-autocomplete>
+ </span>
+ </section>
+ <section>
+ <span class="title">Owner</span>
+ <span class="value">
+ <gr-autocomplete
+ id="ownerInput"
+ .text=${convertToString(this.repoOwner)}
+ .value=${convertToString(this.repoOwnerId)}
+ .query=${this.queryGroups}
+ @text-changed=${this.handleOwnerTextChanged}
+ @value-changed=${this.handleOwnerValueChanged}
+ >
+ </gr-autocomplete>
+ </span>
+ </section>
+ <section>
+ <span class="title">Create initial empty commit</span>
+ <span class="value">
+ <gr-select
+ id="initialCommit"
+ .bindValue=${this.repoConfig.create_empty_commit}
+ @bind-value-changed=${this
+ .handleCreateEmptyCommitBindValueChanged}
+ >
+ <select>
+ <option value="false">False</option>
+ <option value="true">True</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <span class="title"
+ >Only serve as parent for other repositories</span
+ >
+ <span class="value">
+ <gr-select
+ id="parentRepo"
+ .bindValue=${this.repoConfig.permissions_only}
+ @bind-value-changed=${this
+ .handlePermissionsOnlyBindValueChanged}
+ >
+ <select>
+ <option value="false">False</option>
+ <option value="true">True</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ </div>
+ </div>
+ `;
}
_computeRepoUrl(repoName: string) {
@@ -88,44 +202,76 @@
}
override focus() {
- this.shadowRoot?.querySelector('input')?.focus();
+ this.input?.focus();
}
- @observe('_repoConfig.name')
- _updateRepoName(name: string) {
- this.hasNewRepoName = !!name;
+ async handleCreateRepo() {
+ if (this.defaultBranch) this.repoConfig.branches = [this.defaultBranch];
+ if (this.repoOwnerId) this.repoConfig.owners = [this.repoOwnerId];
+ const repoRegistered = await this.restApiService.createRepo(
+ this.repoConfig
+ );
+ if (repoRegistered.status === 201) {
+ this.repoCreated = true;
+ page.show(this._computeRepoUrl(this.repoConfig.name));
+ }
+ return repoRegistered;
}
- handleCreateRepo() {
- if (this._defaultBranch) this._repoConfig.branches = [this._defaultBranch];
- if (this._repoOwnerId) this._repoConfig.owners = [this._repoOwnerId];
- return this.restApiService
- .createRepo(this._repoConfig)
- .then(repoRegistered => {
- if (repoRegistered.status === 201) {
- this._repoCreated = true;
- page.show(this._computeRepoUrl(this._repoConfig.name));
- }
- });
+ private async getRepoSuggestions(input: string) {
+ const response = await this.restApiService.getSuggestedProjects(input);
+
+ const repos = [];
+ for (const [name, project] of Object.entries(response ?? {})) {
+ repos.push({name, value: project.id});
+ }
+ return repos;
}
- _getRepoSuggestions(input: string) {
- return this.restApiService.getSuggestedProjects(input).then(response => {
- const repos = [];
- for (const [name, project] of Object.entries(response ?? {})) {
- repos.push({name, value: project.id});
- }
- return repos;
- });
+ private async getGroupSuggestions(input: string) {
+ const response = await this.restApiService.getSuggestedGroups(input);
+
+ const groups = [];
+ for (const [name, group] of Object.entries(response ?? {})) {
+ groups.push({name, value: decodeURIComponent(group.id)});
+ }
+ return groups;
}
- _getGroupSuggestions(input: string) {
- return this.restApiService.getSuggestedGroups(input).then(response => {
- const groups = [];
- for (const [name, group] of Object.entries(response ?? {})) {
- groups.push({name, value: decodeURIComponent(group.id)});
- }
- return groups;
- });
+ private handleRightsTextChanged(e: CustomEvent) {
+ this.repoConfig.parent = e.detail.value as RepoName;
+ this.requestUpdate();
+ }
+
+ private handleOwnerTextChanged(e: CustomEvent) {
+ this.repoOwner = e.detail.value;
+ }
+
+ private handleOwnerValueChanged(e: CustomEvent) {
+ this.repoOwnerId = e.detail.value as GroupId;
+ }
+
+ private handleNameBindValueChanged(e: CustomEvent) {
+ this.repoConfig.name = e.detail.value as RepoName;
+ // nameChanged needs to be set before the event is fired,
+ // because when the event is fired, gr-repo-list gets
+ // the nameChanged value.
+ this.nameChanged = !!e.detail.value;
+ fireEvent(this, 'new-repo-name');
+ this.requestUpdate();
+ }
+
+ private handleBranchNameBindValueChanged(e: CustomEvent) {
+ this.defaultBranch = e.detail.value as BranchName;
+ }
+
+ private handleCreateEmptyCommitBindValueChanged(e: CustomEvent) {
+ this.repoConfig.create_empty_commit = e.detail.value;
+ this.requestUpdate();
+ }
+
+ private handlePermissionsOnlyBindValueChanged(e: CustomEvent) {
+ this.repoConfig.permissions_only = e.detail.value;
+ this.requestUpdate();
}
}
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts
deleted file mode 100644
index f529ac6..0000000
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-form-styles">
- :host {
- display: inline-block;
- }
- input {
- width: 20em;
- }
- gr-autocomplete {
- width: 20em;
- }
- </style>
-
- <div class="gr-form-styles">
- <div id="form">
- <section>
- <span class="title">Repository name</span>
- <iron-input autocomplete="on" bind-value="{{_repoConfig.name}}">
- <input
- is="iron-input"
- id="repoNameInput"
- autocomplete="on"
- bind-value="{{_repoConfig.name}}"
- />
- </iron-input>
- </section>
- <section>
- <span class="title">Default Branch</span>
- <iron-input autocomplete="off" bind-value="{{_defaultBranch}}">
- <input
- is="iron-input"
- id="defaultBranchNameInput"
- autocomplete="off"
- bind-value="{{_defaultBranch}}"
- />
- </iron-input>
- </section>
- <section>
- <span class="title">Rights inherit from</span>
- <span class="value">
- <gr-autocomplete
- id="rightsInheritFromInput"
- text="{{_repoConfig.parent}}"
- query="[[_query]]"
- placeholder="Optional, defaults to 'All-Projects'"
- >
- </gr-autocomplete>
- </span>
- </section>
- <section>
- <span class="title">Owner</span>
- <span class="value">
- <gr-autocomplete
- id="ownerInput"
- text="{{_repoOwner}}"
- value="{{_repoOwnerId}}"
- query="[[_queryGroups]]"
- >
- </gr-autocomplete>
- </span>
- </section>
- <section>
- <span class="title">Create initial empty commit</span>
- <span class="value">
- <gr-select
- id="initialCommit"
- bind-value="{{_repoConfig.create_empty_commit}}"
- >
- <select>
- <option value="false">False</option>
- <option value="true">True</option>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Only serve as parent for other repositories</span>
- <span class="value">
- <gr-select
- id="parentRepo"
- bind-value="{{_repoConfig.permissions_only}}"
- >
- <select>
- <option value="false">False</option>
- <option value="true">True</option>
- </select>
- </gr-select>
- </span>
- </section>
- </div>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.js b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.js
deleted file mode 100644
index f1babee..0000000
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.js
+++ /dev/null
@@ -1,80 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-create-repo-dialog.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-create-repo-dialog');
-
-suite('gr-create-repo-dialog tests', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- });
-
- test('default values are populated', () => {
- assert.isTrue(element.$.initialCommit.bindValue);
- assert.isFalse(element.$.parentRepo.bindValue);
- });
-
- test('repo created', async () => {
- const configInputObj = {
- name: 'test-repo',
- create_empty_commit: true,
- parent: 'All-Project',
- permissions_only: false,
- };
-
- const saveStub = stubRestApi('createRepo').returns(Promise.resolve({}));
-
- assert.isFalse(element.hasNewRepoName);
-
- element._repoConfig = {
- name: 'test-repo',
- create_empty_commit: true,
- parent: 'All-Project',
- permissions_only: false,
- };
-
- element._repoOwner = 'test';
- element._repoOwnerId = 'testId';
- element._defaultBranch = 'main';
-
- element.$.repoNameInput.bindValue = configInputObj.name;
- element.$.rightsInheritFromInput.bindValue = configInputObj.parent;
- element.$.initialCommit.bindValue =
- configInputObj.create_empty_commit;
- element.$.parentRepo.bindValue =
- configInputObj.permissions_only;
-
- assert.isTrue(element.hasNewRepoName);
-
- assert.deepEqual(element._repoConfig, configInputObj);
-
- await element.handleCreateRepo();
- assert.isTrue(saveStub.lastCall.calledWithExactly(
- {
- ...configInputObj,
- owners: ['testId'],
- branches: ['main'],
- }
- ));
- });
-});
-
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.ts b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.ts
new file mode 100644
index 0000000..d3e2171
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.ts
@@ -0,0 +1,106 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-create-repo-dialog';
+import {GrCreateRepoDialog} from './gr-create-repo-dialog';
+import {
+ mockPromise,
+ queryAndAssert,
+ stubRestApi,
+} from '../../../test/test-utils';
+import {BranchName, GroupId, RepoName} from '../../../types/common';
+import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {GrSelect} from '../../shared/gr-select/gr-select';
+
+const basicFixture = fixtureFromElement('gr-create-repo-dialog');
+
+suite('gr-create-repo-dialog tests', () => {
+ let element: GrCreateRepoDialog;
+
+ setup(async () => {
+ element = basicFixture.instantiate();
+ await element.updateComplete;
+ });
+
+ test('default values are populated', () => {
+ assert.isTrue(
+ queryAndAssert<GrSelect>(element, '#initialCommit').bindValue
+ );
+ assert.isFalse(queryAndAssert<GrSelect>(element, '#parentRepo').bindValue);
+ });
+
+ test('repo created', async () => {
+ const configInputObj = {
+ name: 'test-repo-new' as RepoName,
+ create_empty_commit: true,
+ parent: 'All-Project' as RepoName,
+ permissions_only: false,
+ };
+
+ const saveStub = stubRestApi('createRepo').returns(
+ Promise.resolve(new Response())
+ );
+
+ const promise = mockPromise();
+ element.addEventListener('new-repo-name', () => {
+ promise.resolve();
+ });
+
+ element.repoConfig = {
+ name: 'test-repo' as RepoName,
+ create_empty_commit: true,
+ parent: 'All-Project' as RepoName,
+ permissions_only: false,
+ };
+
+ element.repoOwner = 'test';
+ element.repoOwnerId = 'testId' as GroupId;
+ element.defaultBranch = 'main' as BranchName;
+
+ const repoNameInput = queryAndAssert<HTMLInputElement>(
+ element,
+ '#repoNameInput'
+ );
+ repoNameInput.value = configInputObj.name;
+ repoNameInput.dispatchEvent(
+ new Event('input', {bubbles: true, composed: true})
+ );
+ queryAndAssert<GrAutocomplete>(element, '#rightsInheritFromInput').value =
+ configInputObj.parent;
+ queryAndAssert<GrSelect>(element, '#initialCommit').bindValue =
+ configInputObj.create_empty_commit;
+ queryAndAssert<GrSelect>(element, '#parentRepo').bindValue =
+ configInputObj.permissions_only;
+
+ assert.deepEqual(element.repoConfig, configInputObj);
+
+ await element.handleCreateRepo();
+ assert.isTrue(
+ saveStub.lastCall.calledWithExactly({
+ ...configInputObj,
+ owners: ['testId' as GroupId],
+ branches: ['main' as BranchName],
+ })
+ );
+
+ await promise;
+
+ assert.equal(element.repoConfig.name, configInputObj.name);
+ assert.equal(element.nameChanged, true);
+ });
+});
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts
index 6605350..06dbc5b 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.ts
@@ -15,13 +15,8 @@
* limitations under the License.
*/
-import '../../../styles/gr-table-styles';
-import '../../../styles/shared-styles';
import '../../shared/gr-account-link/gr-account-link';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-group-audit-log_html';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {customElement, property} from '@polymer/decorators';
import {
GroupInfo,
AccountInfo,
@@ -31,58 +26,136 @@
isGroupAuditGroupEventInfo,
} from '../../../types/common';
import {firePageError, fireTitleChange} from '../../../utils/event-util';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {ErrorCallback} from '../../../api/rest';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {tableStyles} from '../../../styles/gr-table-styles';
+import {LitElement, PropertyValues, css, html} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-group-audit-log': GrGroupAuditLog;
+ }
+}
@customElement('gr-group-audit-log')
-export class GrGroupAuditLog extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrGroupAuditLog extends LitElement {
@property({type: String})
groupId?: EncodedGroupId;
- @property({type: Array})
- _auditLog?: GroupAuditEventInfo[];
+ @state() private auditLog?: GroupAuditEventInfo[];
- @property({type: Boolean})
- _loading = true;
+ @state() private loading = true;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
override connectedCallback() {
super.connectedCallback();
fireTitleChange(this, 'Audit Log');
}
- override ready() {
- super.ready();
- this._getAuditLogs();
+ static override get styles() {
+ return [
+ sharedStyles,
+ tableStyles,
+ css`
+ /* GenericList style centers the last column, but we don't want that here. */
+ .genericList tr th:last-of-type,
+ .genericList tr td:last-of-type {
+ text-align: left;
+ }
+ `,
+ ];
}
- _getAuditLogs() {
- if (!this.groupId) {
- return '';
+ override render() {
+ return html`
+ <table id="list" class="genericList">
+ <tbody>
+ <tr class="headerRow">
+ <th class="date topHeader">Date</th>
+ <th class="type topHeader">Type</th>
+ <th class="member topHeader">Member</th>
+ <th class="by-user topHeader">By User</th>
+ </tr>
+ ${this.renderLoading()}
+ </tbody>
+ ${this.renderAuditLogTable()}
+ </table>
+ `;
+ }
+
+ private renderLoading() {
+ if (!this.loading) return;
+
+ return html`
+ <tr id="loading" class="loadingMsg loading">
+ <td>Loading...</td>
+ </tr>
+ `;
+ }
+
+ private renderAuditLogTable() {
+ if (this.loading) return;
+
+ return html`
+ <tbody>
+ ${this.auditLog?.map(audit => this.renderAuditLog(audit))}
+ </tbody>
+ `;
+ }
+
+ private renderAuditLog(audit: GroupAuditEventInfo) {
+ return html`
+ <tr class="table">
+ <td class="date">
+ <gr-date-formatter withTooltip .dateStr=${audit.date}>
+ </gr-date-formatter>
+ </td>
+ <td class="type">${this.itemType(audit.type)}</td>
+ <td class="member">
+ ${this.isGroupEvent(audit)
+ ? html`<a href=${this.computeGroupUrl(audit.member)}
+ >${this.getNameForGroup(audit.member)}</a
+ >`
+ : html`<gr-account-link .account=${audit.member}></gr-account-link
+ >${this.getIdForUser(audit.member)}`}
+ </td>
+ <td class="by-user">
+ <gr-account-link .account=${audit.user}></gr-account-link>
+ ${this.getIdForUser(audit.user)}
+ </td>
+ </tr>
+ `;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('groupId')) {
+ this.getAuditLogs();
}
+ }
+
+ // private but used in test
+ getAuditLogs() {
+ if (!this.groupId) return;
const errFn: ErrorCallback = response => {
firePageError(response);
};
+ this.loading = true;
return this.restApiService
.getGroupAuditLog(this.groupId, errFn)
.then(auditLog => {
- if (!auditLog) {
- this._auditLog = [];
- return;
- }
- this._auditLog = auditLog;
- this._loading = false;
+ this.auditLog = auditLog ?? [];
+ })
+ .finally(() => {
+ this.loading = false;
});
}
- itemType(type: string) {
+ private itemType(type: string) {
let item;
switch (type) {
case 'ADD_GROUP':
@@ -99,11 +172,12 @@
return item;
}
- _isGroupEvent(event: GroupAuditEventInfo): event is GroupAuditGroupEventInfo {
+ // private but used in test
+ isGroupEvent(event: GroupAuditEventInfo): event is GroupAuditGroupEventInfo {
return isGroupAuditGroupEventInfo(event);
}
- _computeGroupUrl(group: GroupInfo) {
+ private computeGroupUrl(group: GroupInfo) {
if (group && group.url && group.id) {
return GerritNav.getUrlForGroup(group.id);
}
@@ -111,11 +185,13 @@
return '';
}
- _getIdForUser(account: AccountInfo) {
+ // private but used in test
+ getIdForUser(account: AccountInfo) {
return account._account_id ? ` (${account._account_id})` : '';
}
- _getNameForGroup(group: GroupInfo) {
+ // private but used in test
+ getNameForGroup(group: GroupInfo) {
if (group && group.name) {
return group.name;
} else if (group && group.id) {
@@ -125,14 +201,4 @@
return '';
}
-
- computeLoadingClass(loading: boolean) {
- return loading ? 'loading' : '';
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-group-audit-log': GrGroupAuditLog;
- }
}
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.ts b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.ts
deleted file mode 100644
index 828aa55..0000000
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-table-styles">
- /* GenericList style centers the last column, but we don't want that here. */
- .genericList tr th:last-of-type,
- .genericList tr td:last-of-type {
- text-align: left;
- }
- </style>
- <table id="list" class="genericList">
- <tbody>
- <tr class="headerRow">
- <th class="date topHeader">Date</th>
- <th class="type topHeader">Type</th>
- <th class="member topHeader">Member</th>
- <th class="by-user topHeader">By User</th>
- </tr>
- <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
- <td>Loading...</td>
- </tr>
- </tbody>
- <tbody class$="[[computeLoadingClass(_loading)]]">
- <template is="dom-repeat" items="[[_auditLog]]">
- <tr class="table">
- <td class="date">
- <gr-date-formatter withTooltip date-str="[[item.date]]">
- </gr-date-formatter>
- </td>
- <td class="type">[[itemType(item.type)]]</td>
- <td class="member">
- <template is="dom-if" if="[[_isGroupEvent(item)]]">
- <a href$="[[_computeGroupUrl(item.member)]]">
- [[_getNameForGroup(item.member)]]
- </a>
- </template>
- <template is="dom-if" if="[[!_isGroupEvent(item)]]">
- <gr-account-link account="[[item.member]]"></gr-account-link>
- [[_getIdForUser(item.member)]]
- </template>
- </td>
- <td class="by-user">
- <gr-account-link account="[[item.user]]"></gr-account-link>
- [[_getIdForUser(item.user)]]
- </td>
- </tr>
- </template>
- </tbody>
- </table>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.ts b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.ts
index dc09390..79b635a 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.ts
@@ -15,67 +15,68 @@
* limitations under the License.
*/
-import '../../../test/common-test-setup-karma.js';
-import './gr-group-audit-log.js';
+import '../../../test/common-test-setup-karma';
+import './gr-group-audit-log';
import {
addListenerForTest,
mockPromise,
stubRestApi,
-} from '../../../test/test-utils.js';
-import {GrGroupAuditLog} from './gr-group-audit-log.js';
+} from '../../../test/test-utils';
+import {GrGroupAuditLog} from './gr-group-audit-log';
import {
EncodedGroupId,
GroupAuditEventType,
GroupInfo,
GroupName,
-} from '../../../types/common.js';
+} from '../../../types/common';
import {
createAccountWithId,
createGroupAuditEventInfo,
createGroupInfo,
-} from '../../../test/test-data-generators.js';
-import {PageErrorEvent} from '../../../types/events.js';
+} from '../../../test/test-data-generators';
+import {PageErrorEvent} from '../../../types/events';
const basicFixture = fixtureFromElement('gr-group-audit-log');
suite('gr-group-audit-log tests', () => {
let element: GrGroupAuditLog;
- setup(() => {
+ setup(async () => {
element = basicFixture.instantiate();
+ await element.updateComplete;
});
suite('members', () => {
- test('test _getNameForGroup', () => {
+ test('test getNameForGroup', () => {
let member: GroupInfo = {
...createGroupInfo(),
name: 'test-name' as GroupName,
};
- assert.equal(element._getNameForGroup(member), 'test-name');
+ assert.equal(element.getNameForGroup(member), 'test-name');
member = createGroupInfo('test-id');
- assert.equal(element._getNameForGroup(member), 'test-id');
+ assert.equal(element.getNameForGroup(member), 'test-id');
});
- test('test _isGroupEvent', () => {
+ test('test isGroupEvent', () => {
assert.isTrue(
- element._isGroupEvent(
+ element.isGroupEvent(
createGroupAuditEventInfo(GroupAuditEventType.ADD_GROUP)
)
);
assert.isTrue(
- element._isGroupEvent(
+ element.isGroupEvent(
createGroupAuditEventInfo(GroupAuditEventType.REMOVE_GROUP)
)
);
assert.isFalse(
- element._isGroupEvent(
+ element.isGroupEvent(
createGroupAuditEventInfo(GroupAuditEventType.ADD_USER)
)
);
assert.isFalse(
- element._isGroupEvent(
+ element.isGroupEvent(
createGroupAuditEventInfo(GroupAuditEventType.REMOVE_USER)
)
);
@@ -83,12 +84,12 @@
});
suite('users', () => {
- test('test _getIdForUser', () => {
+ test('test getIdForUser', () => {
const user = {
...createAccountWithId(12),
username: 'test-user',
};
- assert.equal(element._getIdForUser(user), ' (12)');
+ assert.equal(element.getIdForUser(user), ' (12)');
});
test('test _account_id not present', () => {
@@ -97,14 +98,14 @@
username: 'test-user',
},
};
- assert.equal(element._getIdForUser(account.user), '');
+ assert.equal(element.getIdForUser(account.user), '');
});
});
suite('404', () => {
test('fires page-error', async () => {
element.groupId = '1' as EncodedGroupId;
- await flush();
+ await element.updateComplete;
const response = {...new Response(), status: 404};
stubRestApi('getGroupAuditLog').callsFake((_group, errFn) => {
@@ -118,7 +119,7 @@
pageErrorCalled.resolve();
});
- element._getAuditLogs();
+ element.getAuditLogs();
await pageErrorCalled;
});
});
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 c38f8be..8a0068b 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
@@ -15,20 +15,12 @@
* limitations under the License.
*/
import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
-import '../../../styles/gr-font-styles';
-import '../../../styles/gr-form-styles';
-import '../../../styles/gr-subpage-styles';
-import '../../../styles/gr-table-styles';
-import '../../../styles/shared-styles';
import '../../shared/gr-account-link/gr-account-link';
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 {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-group-members_html';
import {getBaseUrl} from '../../../utils/url-util';
-import {customElement, property} from '@polymer/decorators';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {
GroupId,
@@ -41,15 +33,23 @@
AutocompleteQuery,
AutocompleteSuggestion,
} from '../../shared/gr-autocomplete/gr-autocomplete';
-import {PolymerDomRepeatEvent} from '../../../types/types';
import {
fireAlert,
firePageError,
fireTitleChange,
} from '../../../utils/event-util';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {ErrorCallback} from '../../../api/rest';
import {assertNever} from '../../../utils/common-util';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {subpageStyles} from '../../../styles/gr-subpage-styles';
+import {tableStyles} from '../../../styles/gr-table-styles';
+import {LitElement, css, html} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
+import {ifDefined} from 'lit/directives/if-defined';
const SUGGESTIONS_LIMIT = 15;
const SAVING_ERROR_TEXT =
@@ -62,84 +62,263 @@
INCLUDED_GROUP = 'includedGroup',
}
-export interface GrGroupMembers {
- $: {
- overlay: GrOverlay;
- };
-}
-@customElement('gr-group-members')
-export class GrGroupMembers extends PolymerElement {
- static get template() {
- return htmlTemplate;
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-group-members': GrGroupMembers;
}
+}
- @property({type: Number})
+@customElement('gr-group-members')
+export class GrGroupMembers extends LitElement {
+ @query('#overlay') protected overlay!: GrOverlay;
+
+ @property({type: String})
groupId?: GroupId;
- @property({type: Number})
- _groupMemberSearchId?: number;
+ @state() protected groupMemberSearchId?: number;
- @property({type: String})
- _groupMemberSearchName?: string;
+ @state() protected groupMemberSearchName?: string;
- @property({type: String})
- _includedGroupSearchId?: string;
+ @state() protected includedGroupSearchId?: string;
- @property({type: String})
- _includedGroupSearchName?: string;
+ @state() protected includedGroupSearchName?: string;
- @property({type: Boolean})
- _loading = true;
+ @state() protected loading = true;
- @property({type: String})
- _groupName?: GroupName;
+ /* private but used in test */
+ @state() groupName?: GroupName;
- @property({type: Object})
- _groupMembers?: AccountInfo[];
+ @state() protected groupMembers?: AccountInfo[];
- @property({type: Object})
- _includedGroups?: GroupInfo[];
+ /* private but used in test */
+ @state() includedGroups?: GroupInfo[];
- @property({type: String})
- _itemName?: string;
+ /* private but used in test */
+ @state() itemName?: string;
- @property({type: String})
- _itemType?: ItemType;
+ @state() protected itemType?: ItemType;
- @property({type: Object})
- _queryMembers: AutocompleteQuery;
+ @state() protected queryMembers?: AutocompleteQuery;
- @property({type: Object})
- _queryIncludedGroup: AutocompleteQuery;
+ @state() protected queryIncludedGroup?: AutocompleteQuery;
- @property({type: Boolean})
- _groupOwner = false;
+ /* private but used in test */
+ @state() groupOwner = false;
- @property({type: Boolean})
- _isAdmin = false;
+ @state() protected isAdmin = false;
- _itemId?: AccountId | GroupId;
+ /* private but used in test */
+ @state() itemId?: AccountId | GroupId;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
- this._queryMembers = input => this._getAccountSuggestions(input);
- this._queryIncludedGroup = input => this._getGroupSuggestions(input);
+ this.queryMembers = input => this.getAccountSuggestions(input);
+ this.queryIncludedGroup = input => this.getGroupSuggestions(input);
}
override connectedCallback() {
super.connectedCallback();
- this._loadGroupDetails();
+ this.loadGroupDetails();
fireTitleChange(this, 'Members');
}
- _loadGroupDetails() {
- if (!this.groupId) {
- return;
+ static override get styles() {
+ return [
+ fontStyles,
+ formStyles,
+ sharedStyles,
+ subpageStyles,
+ tableStyles,
+ css`
+ .input {
+ width: 15em;
+ }
+ gr-autocomplete {
+ width: 20em;
+ }
+ a {
+ color: var(--primary-text-color);
+ text-decoration: none;
+ }
+ a:hover {
+ text-decoration: underline;
+ }
+ th {
+ border-bottom: 1px solid var(--border-color);
+ font-weight: var(--font-weight-bold);
+ text-align: left;
+ }
+ .canModify #groupMemberSearchInput,
+ .canModify #saveGroupMember,
+ .canModify .deleteHeader,
+ .canModify .deleteColumn,
+ .canModify #includedGroupSearchInput,
+ .canModify #saveIncludedGroups,
+ .canModify .deleteIncludedHeader,
+ .canModify #saveIncludedGroups {
+ display: none;
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`
+ <div
+ class="main gr-form-styles ${this.isAdmin || this.groupOwner
+ ? ''
+ : 'canModify'}"
+ >
+ <div id="loading" class=${this.loading ? 'loading' : ''}>
+ Loading...
+ </div>
+ <div id="loadedContent" class=${this.loading ? 'loading' : ''}>
+ <h1 id="Title" class="heading-1">${this.groupName}</h1>
+ <div id="form">
+ <h3 id="members" class="heading-3">Members</h3>
+ <fieldset>
+ <span class="value">
+ <gr-autocomplete
+ id="groupMemberSearchInput"
+ .text=${this.groupMemberSearchName}
+ .value=${this.groupMemberSearchId}
+ .query=${this.queryMembers}
+ placeholder="Name Or Email"
+ @text-changed=${this.handleGroupMemberTextChanged}
+ @value-changed=${this.handleGroupMemberValueChanged}
+ >
+ </gr-autocomplete>
+ </span>
+ <gr-button
+ id="saveGroupMember"
+ ?disabled=${!this.groupMemberSearchId}
+ @click=${this.handleSavingGroupMember}
+ >
+ Add
+ </gr-button>
+ <table id="groupMembers">
+ <tbody>
+ <tr class="headerRow">
+ <th class="nameHeader">Name</th>
+ <th class="emailAddressHeader">Email Address</th>
+ <th class="deleteHeader">Delete Member</th>
+ </tr>
+ </tbody>
+ <tbody>
+ ${this.groupMembers?.map((member, index) =>
+ this.renderGroupMember(member, index)
+ )}
+ </tbody>
+ </table>
+ </fieldset>
+ <h3 id="includedGroups" class="heading-3">Included Groups</h3>
+ <fieldset>
+ <span class="value">
+ <gr-autocomplete
+ id="includedGroupSearchInput"
+ .text=${this.includedGroupSearchName}
+ .value=${this.includedGroupSearchId}
+ .query=${this.queryIncludedGroup}
+ placeholder="Group Name"
+ @text-changed=${this.handleIncludedGroupTextChanged}
+ @value-changed=${this.handleIncludedGroupValueChanged}
+ >
+ </gr-autocomplete>
+ </span>
+ <gr-button
+ id="saveIncludedGroups"
+ ?disabled=${!this.includedGroupSearchId}
+ @click=${this.handleSavingIncludedGroups}
+ >
+ Add
+ </gr-button>
+ <table id="includedGroups">
+ <tbody>
+ <tr class="headerRow">
+ <th class="groupNameHeader">Group Name</th>
+ <th class="descriptionHeader">Description</th>
+ <th class="deleteIncludedHeader">Delete Group</th>
+ </tr>
+ </tbody>
+ <tbody>
+ ${this.includedGroups?.map((group, index) =>
+ this.renderIncludedGroup(group, index)
+ )}
+ </tbody>
+ </table>
+ </fieldset>
+ </div>
+ </div>
+ </div>
+ <gr-overlay id="overlay" with-backdrop>
+ <gr-confirm-delete-item-dialog
+ class="confirmDialog"
+ .item=${this.itemName}
+ .itemTypeName=${this.computeItemTypeName(this.itemType)}
+ @confirm=${this.handleDeleteConfirm}
+ @cancel=${this.handleConfirmDialogCancel}
+ ></gr-confirm-delete-item-dialog>
+ </gr-overlay>
+ `;
+ }
+
+ private renderGroupMember(member: AccountInfo, index: number) {
+ return html`
+ <tr>
+ <td class="nameColumn">
+ <gr-account-link .account=${member}></gr-account-link>
+ </td>
+ <td>${member.email}</td>
+ <td class="deleteColumn">
+ <gr-button
+ class="deleteMembersButton"
+ data-index=${index}
+ @click=${this.handleDeleteMember}
+ >
+ Delete
+ </gr-button>
+ </td>
+ </tr>
+ `;
+ }
+
+ private renderIncludedGroup(group: GroupInfo, index: number) {
+ return html`
+ <tr>
+ <td class="nameColumn">${this.renderIncludedGroupHref(group)}</td>
+ <td>${group.description}</td>
+ <td class="deleteColumn">
+ <gr-button
+ class="deleteIncludedGroupButton"
+ data-index=${index}
+ @click=${this.handleDeleteIncludedGroup}
+ >
+ Delete
+ </gr-button>
+ </td>
+ </tr>
+ `;
+ }
+
+ private renderIncludedGroupHref(group: GroupInfo) {
+ if (group.url) {
+ return html`
+ <a href=${ifDefined(this.computeGroupUrl(group.url))} rel="noopener">
+ ${group.name}
+ </a>
+ `;
}
+ return group.name;
+ }
+
+ /* private but used in test */
+ loadGroupDetails() {
+ if (!this.groupId) return;
+
const promises: Promise<void>[] = [];
const errFn: ErrorCallback = response => {
@@ -153,52 +332,43 @@
return Promise.resolve();
}
- this._groupName = config.name;
+ this.groupName = config.name;
promises.push(
this.restApiService.getIsAdmin().then(isAdmin => {
- this._isAdmin = !!isAdmin;
+ this.isAdmin = !!isAdmin;
})
);
promises.push(
- this.restApiService.getIsGroupOwner(this._groupName).then(isOwner => {
- this._groupOwner = !!isOwner;
+ this.restApiService.getIsGroupOwner(this.groupName).then(isOwner => {
+ this.groupOwner = !!isOwner;
})
);
promises.push(
- this.restApiService.getGroupMembers(this._groupName).then(members => {
- this._groupMembers = members;
+ this.restApiService.getGroupMembers(this.groupName).then(members => {
+ this.groupMembers = members;
})
);
promises.push(
this.restApiService
- .getIncludedGroup(this._groupName)
+ .getIncludedGroup(this.groupName)
.then(includedGroup => {
- this._includedGroups = includedGroup;
+ this.includedGroups = includedGroup;
})
);
return Promise.all(promises).then(() => {
- this._loading = false;
+ this.loading = false;
});
});
}
- _computeLoadingClass(loading: boolean) {
- return loading ? 'loading' : '';
- }
-
- _isLoading() {
- return this._loading || this._loading === undefined;
- }
-
- _computeGroupUrl(url: string) {
- if (!url) {
- return;
- }
+ /* private but used in test */
+ computeGroupUrl(url?: string) {
+ if (!url) return;
const r = new RegExp(URL_REGEX, 'i');
if (r.test(url)) {
@@ -212,53 +382,55 @@
return getBaseUrl() + url;
}
- _handleSavingGroupMember() {
- if (!this._groupName) {
+ /* private but used in test */
+ handleSavingGroupMember() {
+ if (!this.groupName) {
return Promise.reject(new Error('group name undefined'));
}
return this.restApiService
- .saveGroupMember(this._groupName, this._groupMemberSearchId as AccountId)
+ .saveGroupMember(this.groupName, this.groupMemberSearchId as AccountId)
.then(config => {
- if (!config || !this._groupName) {
+ if (!config || !this.groupName) {
return;
}
- this.restApiService.getGroupMembers(this._groupName).then(members => {
- this._groupMembers = members;
+ this.restApiService.getGroupMembers(this.groupName).then(members => {
+ this.groupMembers = members;
});
- this._groupMemberSearchName = '';
- this._groupMemberSearchId = undefined;
+ this.groupMemberSearchName = '';
+ this.groupMemberSearchId = undefined;
});
}
- _handleDeleteConfirm() {
- if (!this._groupName) {
+ /* private but used in test */
+ handleDeleteConfirm() {
+ if (!this.groupName) {
return Promise.reject(new Error('group name undefined'));
}
- this.$.overlay.close();
- if (this._itemType === ItemType.MEMBER) {
+ this.overlay.close();
+ if (this.itemType === ItemType.MEMBER) {
return this.restApiService
- .deleteGroupMember(this._groupName, this._itemId! as AccountId)
+ .deleteGroupMember(this.groupName, this.itemId! as AccountId)
.then(itemDeleted => {
- if (itemDeleted.status === 204 && this._groupName) {
+ if (itemDeleted.status === 204 && this.groupName) {
this.restApiService
- .getGroupMembers(this._groupName)
+ .getGroupMembers(this.groupName)
.then(members => {
- this._groupMembers = members;
+ this.groupMembers = members;
});
}
});
- } else if (this._itemType === ItemType.INCLUDED_GROUP) {
+ } else if (this.itemType === ItemType.INCLUDED_GROUP) {
return this.restApiService
- .deleteIncludedGroup(this._groupName, this._itemId! as GroupId)
+ .deleteIncludedGroup(this.groupName, this.itemId! as GroupId)
.then(itemDeleted => {
if (
(itemDeleted.status === 204 || itemDeleted.status === 205) &&
- this._groupName
+ this.groupName
) {
this.restApiService
- .getIncludedGroup(this._groupName)
+ .getIncludedGroup(this.groupName)
.then(includedGroup => {
- this._includedGroups = includedGroup;
+ this.includedGroups = includedGroup;
});
}
});
@@ -266,7 +438,8 @@
return Promise.reject(new Error('Unrecognized item type'));
}
- _computeItemTypeName(itemType?: ItemType): string {
+ /* private but used in test */
+ computeItemTypeName(itemType?: ItemType): string {
if (itemType === undefined) return '';
switch (itemType) {
case ItemType.INCLUDED_GROUP:
@@ -278,35 +451,36 @@
}
}
- _handleConfirmDialogCancel() {
- this.$.overlay.close();
+ private handleConfirmDialogCancel() {
+ this.overlay.close();
}
- _handleDeleteMember(e: PolymerDomRepeatEvent<AccountInfo>) {
- const id = e.model.get('item._account_id');
- const name = e.model.get('item.name');
- const username = e.model.get('item.username');
- const email = e.model.get('item.email');
- const item = username || name || email || id?.toString();
- if (!item) {
- return;
- }
- this._itemName = item;
- this._itemId = id;
- this._itemType = ItemType.MEMBER;
- this.$.overlay.open();
+ private handleDeleteMember(e: Event) {
+ if (!this.groupMembers) return;
+
+ const el = e.target as GrButton;
+ const index = Number(el.getAttribute('data-index')!);
+ const keys = this.groupMembers[index];
+ const item =
+ keys.username || keys.name || keys.email || keys._account_id?.toString();
+ if (!item) return;
+ this.itemName = item;
+ this.itemId = keys._account_id;
+ this.itemType = ItemType.MEMBER;
+ this.overlay.open();
}
- _handleSavingIncludedGroups() {
- if (!this._groupName || !this._includedGroupSearchId) {
+ /* private but used in test */
+ handleSavingIncludedGroups() {
+ if (!this.groupName || !this.includedGroupSearchId) {
return Promise.reject(
new Error('group name or includedGroupSearchId undefined')
);
}
return this.restApiService
.saveIncludedGroup(
- this._groupName,
- this._includedGroupSearchId.replace(/\+/g, ' ') as GroupId,
+ this.groupName,
+ this.includedGroupSearchId.replace(/\+/g, ' ') as GroupId,
(errResponse, err) => {
if (errResponse) {
if (errResponse.status === 404) {
@@ -319,36 +493,38 @@
}
)
.then(config => {
- if (!config || !this._groupName) {
+ if (!config || !this.groupName) {
return;
}
this.restApiService
- .getIncludedGroup(this._groupName)
+ .getIncludedGroup(this.groupName)
.then(includedGroup => {
- this._includedGroups = includedGroup;
+ this.includedGroups = includedGroup;
});
- this._includedGroupSearchName = '';
- this._includedGroupSearchId = '';
+ this.includedGroupSearchName = '';
+ this.includedGroupSearchId = '';
});
}
- _handleDeleteIncludedGroup(e: PolymerDomRepeatEvent<GroupInfo>) {
- const id = decodeURIComponent(`${e.model.get('item.id')}`).replace(
- /\+/g,
- ' '
- ) as GroupId;
- const name = e.model.get('item.name');
+ private handleDeleteIncludedGroup(e: Event) {
+ if (!this.includedGroups) return;
+
+ const el = e.target as GrButton;
+ const index = Number(el.getAttribute('data-index')!);
+ const keys = this.includedGroups[index];
+
+ const id = decodeURIComponent(keys.id).replace(/\+/g, ' ') as GroupId;
+ const name = keys.name;
const item = name || id;
- if (!item) {
- return;
- }
- this._itemName = item;
- this._itemId = id;
- this._itemType = ItemType.INCLUDED_GROUP;
- this.$.overlay.open();
+ if (!item) return;
+ this.itemName = item;
+ this.itemId = id;
+ this.itemType = ItemType.INCLUDED_GROUP;
+ this.overlay.open();
}
- _getAccountSuggestions(input: string) {
+ /* private but used in test */
+ getAccountSuggestions(input: string) {
if (input.length === 0) {
return Promise.resolve([]);
}
@@ -373,7 +549,8 @@
});
}
- _getGroupSuggestions(input: string) {
+ /* private but used in test */
+ getGroupSuggestions(input: string) {
return this.restApiService.getSuggestedGroups(input).then(response => {
const groups: AutocompleteSuggestion[] = [];
for (const [name, group] of Object.entries(response ?? {})) {
@@ -383,13 +560,23 @@
});
}
- _computeHideItemClass(owner: boolean, admin: boolean) {
- return admin || owner ? '' : 'canModify';
+ private handleGroupMemberTextChanged(e: CustomEvent) {
+ if (this.loading) return;
+ this.groupMemberSearchName = e.detail.value;
}
-}
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-group-members': GrGroupMembers;
+ private handleGroupMemberValueChanged(e: CustomEvent) {
+ if (this.loading) return;
+ this.groupMemberSearchId = e.detail.value;
+ }
+
+ private handleIncludedGroupTextChanged(e: CustomEvent) {
+ if (this.loading) return;
+ this.includedGroupSearchName = e.detail.value;
+ }
+
+ private handleIncludedGroupValueChanged(e: CustomEvent) {
+ if (this.loading) return;
+ this.includedGroupSearchId = e.detail.value;
}
}
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.ts b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.ts
deleted file mode 100644
index 518abac..0000000
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.ts
+++ /dev/null
@@ -1,184 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-font-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-form-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-table-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-subpage-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- .input {
- width: 15em;
- }
- gr-autocomplete {
- width: 20em;
- }
- a {
- color: var(--primary-text-color);
- text-decoration: none;
- }
- a:hover {
- text-decoration: underline;
- }
- th {
- border-bottom: 1px solid var(--border-color);
- font-weight: var(--font-weight-bold);
- text-align: left;
- }
- .canModify #groupMemberSearchInput,
- .canModify #saveGroupMember,
- .canModify .deleteHeader,
- .canModify .deleteColumn,
- .canModify #includedGroupSearchInput,
- .canModify #saveIncludedGroups,
- .canModify .deleteIncludedHeader,
- .canModify #saveIncludedGroups {
- display: none;
- }
- </style>
- <div
- class$="main gr-form-styles [[_computeHideItemClass(_groupOwner, _isAdmin)]]"
- >
- <div id="loading" class$="[[_computeLoadingClass(_loading)]]">
- Loading...
- </div>
- <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
- <h1 id="Title" class="heading-1">[[_groupName]]</h1>
- <div id="form">
- <h3 id="members" class="heading-3">Members</h3>
- <fieldset>
- <span class="value">
- <gr-autocomplete
- id="groupMemberSearchInput"
- text="{{_groupMemberSearchName}}"
- value="{{_groupMemberSearchId}}"
- query="[[_queryMembers]]"
- placeholder="Name Or Email"
- >
- </gr-autocomplete>
- </span>
- <gr-button
- id="saveGroupMember"
- on-click="_handleSavingGroupMember"
- disabled="[[!_groupMemberSearchId]]"
- >
- Add
- </gr-button>
- <table id="groupMembers">
- <tbody>
- <tr class="headerRow">
- <th class="nameHeader">Name</th>
- <th class="emailAddressHeader">Email Address</th>
- <th class="deleteHeader">Delete Member</th>
- </tr>
- </tbody>
- <tbody>
- <template is="dom-repeat" items="[[_groupMembers]]">
- <tr>
- <td class="nameColumn">
- <gr-account-link account="[[item]]"></gr-account-link>
- </td>
- <td>[[item.email]]</td>
- <td class="deleteColumn">
- <gr-button
- class="deleteMembersButton"
- on-click="_handleDeleteMember"
- >
- Delete
- </gr-button>
- </td>
- </tr>
- </template>
- </tbody>
- </table>
- </fieldset>
- <h3 id="includedGroups" class="heading-3">Included Groups</h3>
- <fieldset>
- <span class="value">
- <gr-autocomplete
- id="includedGroupSearchInput"
- text="{{_includedGroupSearchName}}"
- value="{{_includedGroupSearchId}}"
- query="[[_queryIncludedGroup]]"
- placeholder="Group Name"
- >
- </gr-autocomplete>
- </span>
- <gr-button
- id="saveIncludedGroups"
- on-click="_handleSavingIncludedGroups"
- disabled="[[!_includedGroupSearchId]]"
- >
- Add
- </gr-button>
- <table id="includedGroups">
- <tbody>
- <tr class="headerRow">
- <th class="groupNameHeader">Group Name</th>
- <th class="descriptionHeader">Description</th>
- <th class="deleteIncludedHeader">Delete Group</th>
- </tr>
- </tbody>
- <tbody>
- <template is="dom-repeat" items="[[_includedGroups]]">
- <tr>
- <td class="nameColumn">
- <template is="dom-if" if="[[item.url]]">
- <a href$="[[_computeGroupUrl(item.url)]]" rel="noopener">
- [[item.name]]
- </a>
- </template>
- <template is="dom-if" if="[[!item.url]]">
- [[item.name]]
- </template>
- </td>
- <td>[[item.description]]</td>
- <td class="deleteColumn">
- <gr-button
- class="deleteIncludedGroupButton"
- on-click="_handleDeleteIncludedGroup"
- >
- Delete
- </gr-button>
- </td>
- </tr>
- </template>
- </tbody>
- </table>
- </fieldset>
- </div>
- </div>
- </div>
- <gr-overlay id="overlay" with-backdrop="">
- <gr-confirm-delete-item-dialog
- class="confirmDialog"
- on-confirm="_handleDeleteConfirm"
- on-cancel="_handleConfirmDialogCancel"
- item="[[_itemName]]"
- item-type-name="[[_computeItemTypeName(_itemType)]]"
- ></gr-confirm-delete-item-dialog>
- </gr-overlay>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js
deleted file mode 100644
index b91b04b..0000000
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js
+++ /dev/null
@@ -1,363 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-group-members.js';
-import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {addListenerForTest, mockPromise, stubBaseUrl, stubRestApi} from '../../../test/test-utils.js';
-import {ItemType} from './gr-group-members.js';
-
-const basicFixture = fixtureFromElement('gr-group-members');
-
-suite('gr-group-members tests', () => {
- let element;
-
- let groups;
- let groupMembers;
- let includedGroups;
- let groupStub;
-
- setup(() => {
- groups = {
- name: 'Administrators',
- owner: 'Administrators',
- group_id: 1,
- };
-
- groupMembers = [
- {
- _account_id: 1000097,
- name: 'Jane Roe',
- email: 'jane.roe@example.com',
- username: 'jane',
- },
- {
- _account_id: 1000096,
- name: 'Test User',
- email: 'john.doe@example.com',
- },
- {
- _account_id: 1000095,
- name: 'Gerrit',
- },
- {
- _account_id: 1000098,
- },
- ];
-
- includedGroups = [{
- url: 'https://group/url',
- options: {},
- id: 'testId',
- name: 'testName',
- },
- {
- url: '/group/url',
- options: {},
- id: 'testId2',
- name: 'testName2',
- },
- {
- url: '#/group/url',
- options: {},
- id: 'testId3',
- name: 'testName3',
- },
- ];
-
- stubRestApi('getSuggestedAccounts').callsFake(input => {
- if (input.startsWith('test')) {
- return Promise.resolve([
- {
- _account_id: 1000096,
- name: 'test-account',
- email: 'test.account@example.com',
- username: 'test123',
- },
- {
- _account_id: 1001439,
- name: 'test-admin',
- email: 'test.admin@example.com',
- username: 'test_admin',
- },
- {
- _account_id: 1001439,
- name: 'test-git',
- username: 'test_git',
- },
- ]);
- } else {
- return Promise.resolve([]);
- }
- });
- stubRestApi('getSuggestedGroups').callsFake(input => {
- if (input.startsWith('test')) {
- return Promise.resolve({
- 'test-admin': {
- id: '1ce023d3fb4e4260776fb92cd08b52bbd21ce70a',
- },
- 'test/Administrator (admin)': {
- id: 'test%3Aadmin',
- },
- });
- } else {
- return Promise.resolve({});
- }
- });
- stubRestApi('getGroupMembers').returns(Promise.resolve(groupMembers));
- stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
- stubRestApi('getIncludedGroup').returns(Promise.resolve(includedGroups));
- element = basicFixture.instantiate();
- stubBaseUrl('https://test/site');
- element.groupId = 1;
- groupStub = stubRestApi('getGroupConfig').returns(Promise.resolve(groups));
- return element._loadGroupDetails();
- });
-
- test('_includedGroups', () => {
- assert.equal(element._includedGroups.length, 3);
- assert.equal(dom(element.root)
- .querySelectorAll('.nameColumn a')[0].href, includedGroups[0].url);
- assert.equal(dom(element.root)
- .querySelectorAll('.nameColumn a')[1].href,
- 'https://test/site/group/url');
- assert.equal(dom(element.root)
- .querySelectorAll('.nameColumn a')[2].href,
- 'https://test/site/group/url');
- });
-
- test('save members correctly', async () => {
- element._groupOwner = true;
-
- const memberName = 'test-admin';
-
- const saveStub = stubRestApi('saveGroupMember')
- .callsFake(() => Promise.resolve({}));
-
- const button = element.$.saveGroupMember;
-
- assert.isTrue(button.hasAttribute('disabled'));
-
- element.$.groupMemberSearchInput.text = memberName;
- element.$.groupMemberSearchInput.value = 1234;
-
- await flush();
- assert.isFalse(button.hasAttribute('disabled'));
-
- return element._handleSavingGroupMember().then(() => {
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- assert.isTrue(saveStub.lastCall.calledWithExactly('Administrators',
- 1234));
- });
- });
-
- test('save included groups correctly', async () => {
- element._groupOwner = true;
-
- const includedGroupName = 'testName';
-
- const saveIncludedGroupStub = stubRestApi('saveIncludedGroup')
- .callsFake(() => Promise.resolve({}));
-
- const button = element.$.saveIncludedGroups;
-
- assert.isTrue(button.hasAttribute('disabled'));
-
- element.$.includedGroupSearchInput.text = includedGroupName;
- element.$.includedGroupSearchInput.value = 'testId';
- await flush();
- assert.isFalse(button.hasAttribute('disabled'));
-
- return element._handleSavingIncludedGroups().then(() => {
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- assert.equal(saveIncludedGroupStub.lastCall.args[0], 'Administrators');
- assert.equal(saveIncludedGroupStub.lastCall.args[1], 'testId');
- });
- });
-
- test('add included group 404 shows helpful error text', () => {
- element._groupOwner = true;
- element._groupName = 'test';
-
- const memberName = 'bad-name';
- const alertStub = sinon.stub();
- element.addEventListener('show-alert', alertStub);
- const errorResponse = {
- status: 404,
- ok: false,
- };
- stubRestApi('saveIncludedGroup').callsFake((
- groupName,
- includedGroup,
- errFn
- ) => {
- errFn(errorResponse);
- return Promise.resolve(undefined);
- });
-
- element.$.groupMemberSearchInput.text = memberName;
- element.$.groupMemberSearchInput.value = 1234;
-
- return flush(element._handleSavingIncludedGroups().then(() => {
- assert.isTrue(alertStub.called);
- }));
- });
-
- test('add included group network-error throws an exception', async () => {
- element._groupOwner = true;
- const memberName = 'bad-name';
- stubRestApi('saveIncludedGroup').throws(new Error());
-
- element.$.groupMemberSearchInput.text = memberName;
- element.$.groupMemberSearchInput.value = 1234;
-
- let exceptionThrown = false;
- try {
- await element._handleSavingIncludedGroups();
- } catch (e) {
- exceptionThrown = true;
- }
- assert.isTrue(exceptionThrown);
- });
-
- test('_getAccountSuggestions empty', async () => {
- const accounts = await element._getAccountSuggestions('nonexistent');
- assert.equal(accounts.length, 0);
- });
-
- test('_getAccountSuggestions non-empty', async () => {
- const accounts = await element._getAccountSuggestions('test-');
- assert.equal(accounts.length, 3);
- assert.equal(accounts[0].name,
- 'test-account <test.account@example.com>');
- assert.equal(accounts[1].name, 'test-admin <test.admin@example.com>');
- assert.equal(accounts[2].name, 'test-git');
- });
-
- test('_getGroupSuggestions empty', async () => {
- const groups = await element._getGroupSuggestions('nonexistent');
-
- assert.equal(groups.length, 0);
- });
-
- test('_getGroupSuggestions non-empty', async () => {
- const groups = await element._getGroupSuggestions('test');
-
- assert.equal(groups.length, 2);
- assert.equal(groups[0].name, 'test-admin');
- assert.equal(groups[1].name, 'test/Administrator (admin)');
- });
-
- test('_computeHideItemClass returns string for admin', () => {
- const admin = true;
- const owner = false;
- assert.equal(element._computeHideItemClass(owner, admin), '');
- });
-
- test('_computeHideItemClass returns hideItem for admin and owner', () => {
- const admin = false;
- const owner = false;
- assert.equal(element._computeHideItemClass(owner, admin), 'canModify');
- });
-
- test('_computeHideItemClass returns string for owner', () => {
- const admin = false;
- const owner = true;
- assert.equal(element._computeHideItemClass(owner, admin), '');
- });
-
- test('delete member', () => {
- const deleteBtns = dom(element.root)
- .querySelectorAll('.deleteMembersButton');
- MockInteractions.tap(deleteBtns[0]);
- assert.equal(element._itemId, '1000097');
- assert.equal(element._itemName, 'jane');
- MockInteractions.tap(deleteBtns[1]);
- assert.equal(element._itemId, '1000096');
- assert.equal(element._itemName, 'Test User');
- MockInteractions.tap(deleteBtns[2]);
- assert.equal(element._itemId, '1000095');
- assert.equal(element._itemName, 'Gerrit');
- MockInteractions.tap(deleteBtns[3]);
- assert.equal(element._itemId, '1000098');
- assert.equal(element._itemName, '1000098');
- });
-
- test('delete included groups', () => {
- const deleteBtns = dom(element.root)
- .querySelectorAll('.deleteIncludedGroupButton');
- MockInteractions.tap(deleteBtns[0]);
- assert.equal(element._itemId, 'testId');
- assert.equal(element._itemName, 'testName');
- MockInteractions.tap(deleteBtns[1]);
- assert.equal(element._itemId, 'testId2');
- assert.equal(element._itemName, 'testName2');
- MockInteractions.tap(deleteBtns[2]);
- assert.equal(element._itemId, 'testId3');
- assert.equal(element._itemName, 'testName3');
- });
-
- test('_computeLoadingClass', () => {
- assert.equal(element._computeLoadingClass(true), 'loading');
-
- assert.equal(element._computeLoadingClass(false), '');
- });
-
- test('_computeGroupUrl', () => {
- assert.isUndefined(element._computeGroupUrl(undefined));
-
- assert.isUndefined(element._computeGroupUrl(false));
-
- let url = '#/admin/groups/uuid-529b3c2605bb1029c8146f9de4a91c776fe64498';
- assert.equal(element._computeGroupUrl(url),
- 'https://test/site/admin/groups/' +
- 'uuid-529b3c2605bb1029c8146f9de4a91c776fe64498');
-
- url = 'https://gerrit.local/admin/groups/' +
- 'uuid-529b3c2605bb1029c8146f9de4a91c776fe64498';
- assert.equal(element._computeGroupUrl(url), url);
- });
-
- test('fires page-error', async () => {
- groupStub.restore();
-
- element.groupId = 1;
-
- const response = {status: 404};
- stubRestApi('getGroupConfig').callsFake((group, errFn) => {
- errFn(response);
- return Promise.resolve();
- });
- const promise = mockPromise();
- addListenerForTest(document, 'page-error', e => {
- assert.deepEqual(e.detail.response, response);
- promise.resolve();
- });
-
- element._loadGroupDetails();
- await promise;
- });
-
- test('_computeItemName', () => {
- assert.equal(element._computeItemTypeName(ItemType.MEMBER), 'Member');
- assert.equal(element._computeItemTypeName(ItemType.INCLUDED_GROUP),
- 'Included Group');
- });
-});
-
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
new file mode 100644
index 0000000..46a69f4
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.ts
@@ -0,0 +1,400 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-group-members';
+import {GrGroupMembers, ItemType} from './gr-group-members';
+import {
+ addListenerForTest,
+ mockPromise,
+ queryAll,
+ queryAndAssert,
+ stubBaseUrl,
+ stubRestApi,
+} from '../../../test/test-utils';
+import {
+ AccountId,
+ AccountInfo,
+ EmailAddress,
+ GroupId,
+ GroupInfo,
+ GroupName,
+} from '../../../types/common';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+import {PageErrorEvent} from '../../../types/events.js';
+
+const basicFixture = fixtureFromElement('gr-group-members');
+
+suite('gr-group-members tests', () => {
+ let element: GrGroupMembers;
+
+ let groups: GroupInfo;
+ let groupMembers: AccountInfo[];
+ let includedGroups: GroupInfo[];
+ let groupStub: sinon.SinonStub;
+
+ setup(async () => {
+ groups = {
+ id: 'testId1' as GroupId,
+ name: 'Administrators' as GroupName,
+ owner: 'Administrators',
+ group_id: 1,
+ };
+
+ groupMembers = [
+ {
+ _account_id: 1000097 as AccountId,
+ name: 'Jane Roe',
+ email: 'jane.roe@example.com' as EmailAddress,
+ username: 'jane',
+ },
+ {
+ _account_id: 1000096 as AccountId,
+ name: 'Test User',
+ email: 'john.doe@example.com' as EmailAddress,
+ },
+ {
+ _account_id: 1000095 as AccountId,
+ name: 'Gerrit',
+ },
+ {
+ _account_id: 1000098 as AccountId,
+ },
+ ];
+
+ includedGroups = [
+ {
+ url: 'https://group/url',
+ options: {
+ visible_to_all: false,
+ },
+ id: 'testId' as GroupId,
+ name: 'testName' as GroupName,
+ },
+ {
+ url: '/group/url',
+ options: {
+ visible_to_all: false,
+ },
+ id: 'testId2' as GroupId,
+ name: 'testName2' as GroupName,
+ },
+ {
+ url: '#/group/url',
+ options: {
+ visible_to_all: false,
+ },
+ id: 'testId3' as GroupId,
+ name: 'testName3' as GroupName,
+ },
+ ];
+
+ stubRestApi('getSuggestedAccounts').callsFake(input => {
+ if (input.startsWith('test')) {
+ return Promise.resolve([
+ {
+ _account_id: 1000096 as AccountId,
+ name: 'test-account',
+ email: 'test.account@example.com' as EmailAddress,
+ username: 'test123',
+ },
+ {
+ _account_id: 1001439 as AccountId,
+ name: 'test-admin',
+ email: 'test.admin@example.com' as EmailAddress,
+ username: 'test_admin',
+ },
+ {
+ _account_id: 1001439 as AccountId,
+ name: 'test-git',
+ username: 'test_git',
+ },
+ ]);
+ } else {
+ return Promise.resolve([]);
+ }
+ });
+ stubRestApi('getSuggestedGroups').callsFake(input => {
+ if (input.startsWith('test')) {
+ return Promise.resolve({
+ 'test-admin': {
+ id: '1ce023d3fb4e4260776fb92cd08b52bbd21ce70a',
+ },
+ 'test/Administrator (admin)': {
+ id: 'test%3Aadmin',
+ },
+ });
+ } else {
+ return Promise.resolve({});
+ }
+ });
+ stubRestApi('getGroupMembers').returns(Promise.resolve(groupMembers));
+ stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
+ stubRestApi('getIncludedGroup').returns(Promise.resolve(includedGroups));
+ element = basicFixture.instantiate();
+ await element.updateComplete;
+ stubBaseUrl('https://test/site');
+ element.groupId = 'testId1' as GroupId;
+ groupStub = stubRestApi('getGroupConfig').returns(Promise.resolve(groups));
+ return element.loadGroupDetails();
+ });
+
+ test('includedGroups', () => {
+ assert.equal(element.includedGroups!.length, 3);
+ assert.equal(
+ queryAll<HTMLAnchorElement>(element, '.nameColumn a')[0].href,
+ includedGroups[0].url
+ );
+ assert.equal(
+ queryAll<HTMLAnchorElement>(element, '.nameColumn a')[1].href,
+ 'https://test/site/group/url'
+ );
+ assert.equal(
+ queryAll<HTMLAnchorElement>(element, '.nameColumn a')[2].href,
+ 'https://test/site/group/url'
+ );
+ });
+
+ test('save members correctly', async () => {
+ element.groupOwner = true;
+
+ const memberName = 'test-admin';
+
+ const saveStub = stubRestApi('saveGroupMember').callsFake(() =>
+ Promise.resolve({})
+ );
+
+ const button = queryAndAssert<GrButton>(element, '#saveGroupMember');
+
+ assert.isTrue(button.hasAttribute('disabled'));
+
+ const groupMemberSearchInput = queryAndAssert<GrAutocomplete>(
+ element,
+ '#groupMemberSearchInput'
+ );
+ groupMemberSearchInput.text = memberName;
+ groupMemberSearchInput.value = '1234';
+
+ await element.updateComplete;
+ assert.isFalse(button.hasAttribute('disabled'));
+
+ return element.handleSavingGroupMember().then(() => {
+ assert.isTrue(button.hasAttribute('disabled'));
+ assert.isFalse(
+ queryAndAssert<HTMLHeadingElement>(
+ element,
+ '#Title'
+ ).classList.contains('edited')
+ );
+ assert.equal(saveStub.lastCall.args[0], 'Administrators');
+ assert.equal(saveStub.lastCall.args[1], 1234);
+ });
+ });
+
+ test('save included groups correctly', async () => {
+ element.groupOwner = true;
+
+ const includedGroupName = 'testName';
+
+ const saveIncludedGroupStub = stubRestApi('saveIncludedGroup').callsFake(
+ () => Promise.resolve({id: '0' as GroupId})
+ );
+
+ const button = queryAndAssert<GrButton>(element, '#saveIncludedGroups');
+
+ assert.isTrue(button.hasAttribute('disabled'));
+
+ const includedGroupSearchInput = queryAndAssert<GrAutocomplete>(
+ element,
+ '#includedGroupSearchInput'
+ );
+ includedGroupSearchInput.text = includedGroupName;
+ includedGroupSearchInput.value = 'testId';
+ await element.updateComplete;
+ assert.isFalse(button.hasAttribute('disabled'));
+
+ return element.handleSavingIncludedGroups().then(() => {
+ assert.isTrue(button.hasAttribute('disabled'));
+ assert.isFalse(
+ queryAndAssert<HTMLHeadingElement>(
+ element,
+ '#Title'
+ ).classList.contains('edited')
+ );
+ assert.equal(saveIncludedGroupStub.lastCall.args[0], 'Administrators');
+ assert.equal(saveIncludedGroupStub.lastCall.args[1], 'testId');
+ });
+ });
+
+ test('add included group 404 shows helpful error text', async () => {
+ element.groupOwner = true;
+ element.groupName = 'test' as GroupName;
+
+ const memberName = 'bad-name';
+ const alertStub = sinon.stub();
+ element.addEventListener('show-alert', alertStub);
+ const errorResponse = {...new Response(), status: 404, ok: false};
+ stubRestApi('saveIncludedGroup').callsFake((_, _non, errFn) => {
+ if (errFn !== undefined) {
+ errFn(errorResponse);
+ } else {
+ assert.fail('errFn is undefined');
+ }
+ return Promise.resolve(undefined);
+ });
+
+ const groupMemberSearchInput = queryAndAssert<GrAutocomplete>(
+ element,
+ '#groupMemberSearchInput'
+ );
+ groupMemberSearchInput.text = memberName;
+ groupMemberSearchInput.value = '1234';
+
+ await element.updateComplete;
+ element.handleSavingIncludedGroups().then(() => {
+ assert.isTrue(alertStub.called);
+ });
+ });
+
+ test('add included group network-error throws an exception', async () => {
+ element.groupOwner = true;
+ const memberName = 'bad-name';
+ stubRestApi('saveIncludedGroup').throws(new Error());
+
+ const groupMemberSearchInput = queryAndAssert<GrAutocomplete>(
+ element,
+ '#groupMemberSearchInput'
+ );
+ groupMemberSearchInput.text = memberName;
+ groupMemberSearchInput.value = '1234';
+
+ let exceptionThrown = false;
+ try {
+ await element.handleSavingIncludedGroups();
+ } catch (e) {
+ exceptionThrown = true;
+ }
+ assert.isTrue(exceptionThrown);
+ });
+
+ test('getAccountSuggestions empty', async () => {
+ const accounts = await element.getAccountSuggestions('nonexistent');
+ assert.equal(accounts.length, 0);
+ });
+
+ test('getAccountSuggestions non-empty', async () => {
+ const accounts = await element.getAccountSuggestions('test-');
+ assert.equal(accounts.length, 3);
+ assert.equal(accounts[0].name, 'test-account <test.account@example.com>');
+ assert.equal(accounts[1].name, 'test-admin <test.admin@example.com>');
+ assert.equal(accounts[2].name, 'test-git');
+ });
+
+ test('getGroupSuggestions empty', async () => {
+ const groups = await element.getGroupSuggestions('nonexistent');
+
+ assert.equal(groups.length, 0);
+ });
+
+ test('getGroupSuggestions non-empty', async () => {
+ const groups = await element.getGroupSuggestions('test');
+
+ assert.equal(groups.length, 2);
+ assert.equal(groups[0].name, 'test-admin');
+ assert.equal(groups[1].name, 'test/Administrator (admin)');
+ });
+
+ test('delete member', () => {
+ const deleteBtns = queryAll<GrButton>(element, '.deleteMembersButton');
+ MockInteractions.tap(deleteBtns[0]);
+ assert.equal(element.itemId, 1000097 as AccountId);
+ assert.equal(element.itemName, 'jane');
+ MockInteractions.tap(deleteBtns[1]);
+ assert.equal(element.itemId, 1000096 as AccountId);
+ assert.equal(element.itemName, 'Test User');
+ MockInteractions.tap(deleteBtns[2]);
+ assert.equal(element.itemId, 1000095 as AccountId);
+ assert.equal(element.itemName, 'Gerrit');
+ MockInteractions.tap(deleteBtns[3]);
+ assert.equal(element.itemId, 1000098 as AccountId);
+ assert.equal(element.itemName, '1000098');
+ });
+
+ test('delete included groups', () => {
+ const deleteBtns = queryAll<GrButton>(
+ element,
+ '.deleteIncludedGroupButton'
+ );
+ MockInteractions.tap(deleteBtns[0]);
+ assert.equal(element.itemId, 'testId' as GroupId);
+ assert.equal(element.itemName, 'testName');
+ MockInteractions.tap(deleteBtns[1]);
+ assert.equal(element.itemId, 'testId2' as GroupId);
+ assert.equal(element.itemName, 'testName2');
+ MockInteractions.tap(deleteBtns[2]);
+ assert.equal(element.itemId, 'testId3' as GroupId);
+ assert.equal(element.itemName, 'testName3');
+ });
+
+ test('computeGroupUrl', () => {
+ assert.isUndefined(element.computeGroupUrl(undefined));
+
+ let url = '#/admin/groups/uuid-529b3c2605bb1029c8146f9de4a91c776fe64498';
+ assert.equal(
+ element.computeGroupUrl(url),
+ 'https://test/site/admin/groups/' +
+ 'uuid-529b3c2605bb1029c8146f9de4a91c776fe64498'
+ );
+
+ url =
+ 'https://gerrit.local/admin/groups/' +
+ 'uuid-529b3c2605bb1029c8146f9de4a91c776fe64498';
+ assert.equal(element.computeGroupUrl(url), url);
+ });
+
+ test('fires page-error', async () => {
+ groupStub.restore();
+
+ element.groupId = 'testId1' as GroupId;
+
+ const response = {...new Response(), status: 404};
+ stubRestApi('getGroupConfig').callsFake((_, errFn) => {
+ if (errFn !== undefined) {
+ errFn(response);
+ }
+ return Promise.resolve(undefined);
+ });
+ const promise = mockPromise();
+ addListenerForTest(document, 'page-error', e => {
+ assert.deepEqual((e as PageErrorEvent).detail.response, response);
+ promise.resolve();
+ });
+
+ element.loadGroupDetails();
+ await promise;
+ });
+
+ test('_computeItemName', () => {
+ assert.equal(element.computeItemTypeName(ItemType.MEMBER), 'Member');
+ assert.equal(
+ element.computeItemTypeName(ItemType.INCLUDED_GROUP),
+ 'Included Group'
+ );
+ });
+});
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts b/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
index 596fe5b..8faa5e7 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
@@ -15,29 +15,27 @@
* limitations under the License.
*/
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
-import '../../../styles/gr-font-styles';
-import '../../../styles/gr-form-styles';
-import '../../../styles/gr-subpage-styles';
-import '../../../styles/shared-styles';
import '../../shared/gr-autocomplete/gr-autocomplete';
+import '../../shared/gr-button/gr-button';
import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
import '../../shared/gr-select/gr-select';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-group_html';
-import {customElement, property, observe} from '@polymer/decorators';
+import '../../shared/gr-textarea/gr-textarea';
import {
AutocompleteSuggestion,
AutocompleteQuery,
} from '../../shared/gr-autocomplete/gr-autocomplete';
import {GroupId, GroupInfo, GroupName} from '../../../types/common';
-import {
- fireEvent,
- firePageError,
- fireTitleChange,
-} from '../../../utils/event-util';
-import {appContext} from '../../../services/app-context';
+import {firePageError, fireTitleChange} from '../../../utils/event-util';
+import {getAppContext} from '../../../services/app-context';
import {ErrorCallback} from '../../../api/rest';
+import {convertToString} from '../../../utils/string-util';
+import {BindValueChangeEvent} from '../../../types/events';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {subpageStyles} from '../../../styles/gr-subpage-styles';
+import {LitElement, PropertyValues, css, html} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
@@ -52,93 +50,279 @@
},
};
-export interface GrGroup {
- $: {
- loading: HTMLDivElement;
- };
-}
-
export interface GroupNameChangedDetail {
name: GroupName;
external: boolean;
}
declare global {
+ interface HTMLElementEventMap {
+ 'text-changed': CustomEvent;
+ 'value-changed': CustomEvent;
+ }
interface HTMLElementTagNameMap {
'gr-group': GrGroup;
}
}
@customElement('gr-group')
-export class GrGroup extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrGroup extends LitElement {
/**
* Fired when the group name changes.
*
* @event name-changed
*/
+ private readonly query: AutocompleteQuery;
+
@property({type: String})
groupId?: GroupId;
- @property({type: Boolean})
- _rename = false;
+ @state() private originalOwnerName?: string;
- @property({type: Boolean})
- _groupIsInternal = false;
+ @state() private originalDescriptionName?: string;
- @property({type: Boolean})
- _description = false;
+ @state() private originalOptionsVisibleToAll?: boolean;
- @property({type: Boolean})
- _owner = false;
+ @state() private submitTypes = Object.values(OPTIONS);
- @property({type: Boolean})
- _options = false;
+ // private but used in test
+ @state() isAdmin = false;
- @property({type: Boolean})
- _loading = true;
+ // private but used in test
+ @state() groupOwner = false;
- @property({type: Object})
- _groupConfig?: GroupInfo;
+ // private but used in test
+ @state() groupIsInternal = false;
- @property({type: String})
- _groupConfigOwner?: string;
+ // private but used in test
+ @state() loading = true;
- @property({type: Object})
- _groupName?: string;
+ // private but used in test
+ @state() groupConfig?: GroupInfo;
- @property({type: Boolean})
- _groupOwner = false;
+ // private but used in test
+ @state() groupConfigOwner?: string;
- @property({type: Array})
- _submitTypes = Object.values(OPTIONS);
+ // private but used in test
+ @state() originalName?: GroupName;
- @property({type: Object})
- _query: AutocompleteQuery;
-
- @property({type: Boolean})
- _isAdmin = false;
-
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
- this._query = (input: string) => this._getGroupSuggestions(input);
+ this.query = (input: string) => this.getGroupSuggestions(input);
}
override connectedCallback() {
super.connectedCallback();
- this._loadGroup();
}
- _loadGroup() {
- if (!this.groupId) {
- return;
+ static override get styles() {
+ return [
+ fontStyles,
+ formStyles,
+ sharedStyles,
+ subpageStyles,
+ css`
+ h3.edited:after {
+ color: var(--deemphasized-text-color);
+ content: ' *';
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`
+ <div class="main gr-form-styles read-only">
+ <div id="loading" class="${this.computeLoadingClass()}">Loading...</div>
+ <div id="loadedContent" class="${this.computeLoadingClass()}">
+ <h1 id="Title" class="heading-1">${this.originalName}</h1>
+ <h2 id="configurations" class="heading-2">General</h2>
+ <div id="form">
+ <fieldset>
+ ${this.renderGroupUUID()} ${this.renderGroupName()}
+ ${this.renderGroupOwner()} ${this.renderGroupDescription()}
+ ${this.renderGroupOptions()}
+ </fieldset>
+ </div>
+ </div>
+ </div>
+ `;
+ }
+
+ private renderGroupUUID() {
+ return html`
+ <h3 id="groupUUID" class="heading-3">Group UUID</h3>
+ <fieldset>
+ <gr-copy-clipboard
+ id="uuid"
+ .text=${this.getGroupUUID()}
+ ></gr-copy-clipboard>
+ </fieldset>
+ `;
+ }
+
+ private renderGroupName() {
+ const groupNameEdited = this.originalName !== this.groupConfig?.name;
+ return html`
+ <h3
+ id="groupName"
+ class="heading-3 ${this.computeHeaderClass(groupNameEdited)}"
+ >
+ Group Name
+ </h3>
+ <fieldset>
+ <span class="value">
+ <gr-autocomplete
+ id="groupNameInput"
+ .text=${this.groupConfig?.name}
+ ?disabled=${this.computeGroupDisabled()}
+ @text-changed=${this.handleNameTextChanged}
+ ></gr-autocomplete>
+ </span>
+ <span class="value">
+ <gr-button
+ id="inputUpdateNameBtn"
+ ?disabled=${!groupNameEdited}
+ @click=${this.handleSaveName}
+ >
+ Rename Group</gr-button
+ >
+ </span>
+ </fieldset>
+ `;
+ }
+
+ private renderGroupOwner() {
+ const groupOwnerNameEdited =
+ this.originalOwnerName !== this.groupConfig?.owner;
+ return html`
+ <h3
+ id="groupOwner"
+ class="heading-3 ${this.computeHeaderClass(groupOwnerNameEdited)}"
+ >
+ Owners
+ </h3>
+ <fieldset>
+ <span class="value">
+ <gr-autocomplete
+ id="groupOwnerInput"
+ .text=${this.groupConfig?.owner}
+ .value=${this.groupConfigOwner}
+ .query=${this.query}
+ ?disabled=${this.computeGroupDisabled()}
+ @text-changed=${this.handleOwnerTextChanged}
+ @value-changed=${this.handleOwnerValueChanged}
+ >
+ </gr-autocomplete>
+ </span>
+ <span class="value">
+ <gr-button
+ id="inputUpdateOwnerBtn"
+ ?disabled=${!groupOwnerNameEdited}
+ @click=${this.handleSaveOwner}
+ >
+ Change Owners</gr-button
+ >
+ </span>
+ </fieldset>
+ `;
+ }
+
+ private renderGroupDescription() {
+ const groupDescriptionEdited =
+ this.originalDescriptionName !== this.groupConfig?.description;
+ return html`
+ <h3 class="heading-3 ${this.computeHeaderClass(groupDescriptionEdited)}">
+ Description
+ </h3>
+ <fieldset>
+ <div>
+ <gr-textarea
+ class="description"
+ autocomplete="on"
+ rows="4"
+ monospace
+ ?disabled=${this.computeGroupDisabled()}
+ .text=${this.groupConfig?.description}
+ @text-changed=${this.handleDescriptionTextChanged}
+ ></gr-textarea>
+ </div>
+ <span class="value">
+ <gr-button
+ ?disabled=${!groupDescriptionEdited}
+ @click=${this.handleSaveDescription}
+ >
+ Save Description
+ </gr-button>
+ </span>
+ </fieldset>
+ `;
+ }
+
+ private renderGroupOptions() {
+ // We make sure the value is a boolean
+ // this is done so undefined is converted to false.
+ const groupOptionsEdited =
+ Boolean(this.originalOptionsVisibleToAll) !==
+ Boolean(this.groupConfig?.options?.visible_to_all);
+
+ // We have to convert boolean to string in order
+ // for the selection to work correctly.
+ // We also convert undefined to false using boolean.
+ return html`
+ <h3
+ id="options"
+ class="heading-3 ${this.computeHeaderClass(groupOptionsEdited)}"
+ >
+ Group Options
+ </h3>
+ <fieldset>
+ <section>
+ <span class="title">
+ Make group visible to all registered users
+ </span>
+ <span class="value">
+ <gr-select
+ id="visibleToAll"
+ .bindValue="${convertToString(
+ Boolean(this.groupConfig?.options?.visible_to_all)
+ )}"
+ @bind-value-changed=${this.handleOptionsBindValueChanged}
+ >
+ <select ?disabled=${this.computeGroupDisabled()}>
+ ${this.submitTypes.map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <span class="value">
+ <gr-button
+ ?disabled=${!groupOptionsEdited}
+ @click=${this.handleSaveOptions}
+ >
+ Save Group Options
+ </gr-button>
+ </span>
+ </fieldset>
+ `;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('groupId')) {
+ this.loadGroup();
}
+ }
+
+ // private but used in test
+ async loadGroup() {
+ if (!this.groupId) return;
const promises: Promise<unknown>[] = [];
@@ -146,154 +330,119 @@
firePageError(response);
};
- return this.restApiService
- .getGroupConfig(this.groupId, errFn)
- .then(config => {
- if (!config || !config.name) {
- return Promise.resolve();
- }
+ const config = await this.restApiService.getGroupConfig(
+ this.groupId,
+ errFn
+ );
+ if (!config || !config.name) return;
- this._groupName = config.name;
- this._groupIsInternal = !!config.id.match(INTERNAL_GROUP_REGEX);
+ if (config.description === undefined) {
+ config.description = '';
+ }
- promises.push(
- this.restApiService.getIsAdmin().then(isAdmin => {
- this._isAdmin = !!isAdmin;
- })
- );
+ this.originalName = config.name;
+ this.originalOwnerName = config.owner;
+ this.originalDescriptionName = config.description;
+ this.groupIsInternal = !!config.id.match(INTERNAL_GROUP_REGEX);
- promises.push(
- this.restApiService.getIsGroupOwner(config.name).then(isOwner => {
- this._groupOwner = !!isOwner;
- })
- );
+ promises.push(
+ this.restApiService.getIsAdmin().then(isAdmin => {
+ this.isAdmin = !!isAdmin;
+ })
+ );
- // If visible to all is undefined, set to false. If it is defined
- // as false, setting to false is fine. If any optional values
- // are added with a default of true, then this would need to be an
- // undefined check and not a truthy/falsy check.
- if (config.options && !config.options.visible_to_all) {
- config.options.visible_to_all = false;
- }
- this._groupConfig = config;
+ promises.push(
+ this.restApiService.getIsGroupOwner(config.name).then(isOwner => {
+ this.groupOwner = !!isOwner;
+ })
+ );
- fireTitleChange(this, config.name);
+ this.groupConfig = config;
+ this.originalOptionsVisibleToAll = config?.options?.visible_to_all;
- return Promise.all(promises).then(() => {
- this._loading = false;
- });
- });
+ fireTitleChange(this, config.name);
+
+ await Promise.all(promises);
+ this.loading = false;
}
- _computeLoadingClass(loading: boolean) {
- return loading ? 'loading' : '';
+ // private but used in test
+ computeLoadingClass() {
+ return this.loading ? 'loading' : '';
}
- _isLoading() {
- return this._loading || this._loading === undefined;
- }
-
- _handleSaveName() {
- const groupConfig = this._groupConfig;
+ // private but used in test
+ async handleSaveName() {
+ const groupConfig = this.groupConfig;
if (!this.groupId || !groupConfig || !groupConfig.name) {
return Promise.reject(new Error('invalid groupId or config name'));
}
const groupName = groupConfig.name;
- return this.restApiService
- .saveGroupName(this.groupId, groupName)
- .then(config => {
- if (config.status === 200) {
- this._groupName = groupName;
- const detail: GroupNameChangedDetail = {
- name: groupName,
- external: !this._groupIsInternal,
- };
- fireEvent(this, 'name-changed');
- this.dispatchEvent(
- new CustomEvent('name-changed', {
- detail,
- composed: true,
- bubbles: true,
- })
- );
- this._rename = false;
- }
- });
+ const config = await this.restApiService.saveGroupName(
+ this.groupId,
+ groupName
+ );
+ if (config.status === 200) {
+ this.originalName = groupName;
+ const detail: GroupNameChangedDetail = {
+ name: groupName,
+ external: !this.groupIsInternal,
+ };
+ this.dispatchEvent(
+ new CustomEvent('name-changed', {
+ detail,
+ composed: true,
+ bubbles: true,
+ })
+ );
+ this.requestUpdate();
+ }
+
+ return;
}
- _handleSaveOwner() {
- if (!this.groupId || !this._groupConfig) return;
- let owner = this._groupConfig.owner;
- if (this._groupConfigOwner) {
- owner = decodeURIComponent(this._groupConfigOwner);
+ // private but used in test
+ async handleSaveOwner() {
+ if (!this.groupId || !this.groupConfig) return;
+ let owner = this.groupConfig.owner;
+ if (this.groupConfigOwner) {
+ owner = decodeURIComponent(this.groupConfigOwner);
}
if (!owner) return;
- return this.restApiService.saveGroupOwner(this.groupId, owner).then(() => {
- this._owner = false;
- });
+ await this.restApiService.saveGroupOwner(this.groupId, owner);
+ this.originalOwnerName = this.groupConfig?.owner;
+ this.groupConfigOwner = undefined;
}
- _handleSaveDescription() {
- if (!this.groupId || !this._groupConfig || !this._groupConfig.description)
+ // private but used in test
+ async handleSaveDescription() {
+ if (
+ !this.groupId ||
+ !this.groupConfig ||
+ this.groupConfig.description === undefined
+ )
return;
- return this.restApiService
- .saveGroupDescription(this.groupId, this._groupConfig.description)
- .then(() => {
- this._description = false;
- });
+ await this.restApiService.saveGroupDescription(
+ this.groupId,
+ this.groupConfig.description
+ );
+ this.originalDescriptionName = this.groupConfig.description;
}
- _handleSaveOptions() {
- if (!this.groupId || !this._groupConfig || !this._groupConfig.options)
- return;
- const visible = this._groupConfig.options.visible_to_all;
-
+ // private but used in test
+ async handleSaveOptions() {
+ if (!this.groupId || !this.groupConfig || !this.groupConfig.options) return;
+ const visible = this.groupConfig.options.visible_to_all;
const options = {visible_to_all: visible};
-
- return this.restApiService
- .saveGroupOptions(this.groupId, options)
- .then(() => {
- this._options = false;
- });
+ await this.restApiService.saveGroupOptions(this.groupId, options);
+ this.originalOptionsVisibleToAll = visible;
}
- @observe('_groupConfig.name')
- _handleConfigName() {
- if (this._isLoading()) {
- return;
- }
- this._rename = true;
- }
-
- @observe('_groupConfig.owner', '_groupConfigOwner')
- _handleConfigOwner() {
- if (this._isLoading()) {
- return;
- }
- this._owner = true;
- }
-
- @observe('_groupConfig.description')
- _handleConfigDescription() {
- if (this._isLoading()) {
- return;
- }
- this._description = true;
- }
-
- @observe('_groupConfig.options.visible_to_all')
- _handleConfigOptions() {
- if (this._isLoading()) {
- return;
- }
- this._options = true;
- }
-
- _computeHeaderClass(configChanged: boolean) {
+ private computeHeaderClass(configChanged: boolean) {
return configChanged ? 'edited' : '';
}
- _getGroupSuggestions(input: string) {
+ private getGroupSuggestions(input: string) {
return this.restApiService.getSuggestedGroups(input).then(response => {
const groups: AutocompleteSuggestion[] = [];
for (const [name, group] of Object.entries(response ?? {})) {
@@ -303,17 +452,48 @@
});
}
- _computeGroupDisabled(
- owner: boolean,
- admin: boolean,
- groupIsInternal: boolean
- ) {
- return !(groupIsInternal && (admin || owner));
+ // private but used in test
+ computeGroupDisabled() {
+ return !(this.groupIsInternal && (this.isAdmin || this.groupOwner));
}
- _getGroupUUID(id: GroupId) {
+ private getGroupUUID() {
+ const id = this.groupConfig?.id;
if (!id) return;
-
return id.match(INTERNAL_GROUP_REGEX) ? id : decodeURIComponent(id);
}
+
+ private handleNameTextChanged(e: CustomEvent) {
+ if (!this.groupConfig || this.loading) return;
+ this.groupConfig.name = e.detail.value as GroupName;
+ this.requestUpdate();
+ }
+
+ private handleOwnerTextChanged(e: CustomEvent) {
+ if (!this.groupConfig || this.loading) return;
+ this.groupConfig.owner = e.detail.value;
+ this.requestUpdate();
+ }
+
+ private handleOwnerValueChanged(e: CustomEvent) {
+ if (this.loading) return;
+ this.groupConfigOwner = e.detail.value;
+ this.requestUpdate();
+ }
+
+ private handleDescriptionTextChanged(e: CustomEvent) {
+ if (!this.groupConfig || this.loading) return;
+ this.groupConfig.description = e.detail.value;
+ this.requestUpdate();
+ }
+
+ private handleOptionsBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.groupConfig || this.loading) return;
+
+ // Because the value for e.detail.value is a string
+ // we convert the value to a boolean.
+ const value = e.detail.value === 'true' ? true : false;
+ this.groupConfig!.options!.visible_to_all = value;
+ this.requestUpdate();
+ }
}
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts
deleted file mode 100644
index 6bc5d2a..0000000
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts
+++ /dev/null
@@ -1,168 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-font-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-subpage-styles">
- h3.edited:after {
- color: var(--deemphasized-text-color);
- content: ' *';
- }
- </style>
- <style include="gr-form-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <div class="main gr-form-styles read-only">
- <div id="loading" class$="[[_computeLoadingClass(_loading)]]">
- Loading...
- </div>
- <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
- <h1 id="Title" class="heading-1">[[_groupName]]</h1>
- <h2 id="configurations" class="heading-2">General</h2>
- <div id="form">
- <fieldset>
- <h3 id="groupUUID" class="heading-3">Group UUID</h3>
- <fieldset>
- <gr-copy-clipboard
- id="uuid"
- text="[[_getGroupUUID(_groupConfig.id)]]"
- ></gr-copy-clipboard>
- </fieldset>
- <h3
- id="groupName"
- class$="heading-3 [[_computeHeaderClass(_rename)]]"
- >
- Group Name
- </h3>
- <fieldset>
- <span class="value">
- <gr-autocomplete
- id="groupNameInput"
- text="{{_groupConfig.name}}"
- disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- ></gr-autocomplete>
- </span>
- <span
- class="value"
- disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- >
- <gr-button
- id="inputUpdateNameBtn"
- on-click="_handleSaveName"
- disabled="[[!_rename]]"
- >
- Rename Group</gr-button
- >
- </span>
- </fieldset>
- <h3
- id="groupOwner"
- class$="heading-3 [[_computeHeaderClass(_owner)]]"
- >
- Owners
- </h3>
- <fieldset>
- <span class="value">
- <gr-autocomplete
- id="groupOwnerInput"
- text="{{_groupConfig.owner}}"
- value="{{_groupConfigOwner}}"
- query="[[_query]]"
- disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- >
- </gr-autocomplete>
- </span>
- <span
- class="value"
- disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- >
- <gr-button
- id="inputUpdateOwnerBtn"
- on-click="_handleSaveOwner"
- disabled="[[!_owner]]"
- >
- Change Owners</gr-button
- >
- </span>
- </fieldset>
- <h3 class$="heading-3 [[_computeHeaderClass(_description)]]">
- Description
- </h3>
- <fieldset>
- <div>
- <iron-autogrow-textarea
- class="description"
- autocomplete="on"
- bind-value="{{_groupConfig.description}}"
- disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- ></iron-autogrow-textarea>
- </div>
- <span
- class="value"
- disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- >
- <gr-button
- on-click="_handleSaveDescription"
- disabled="[[!_description]]"
- >
- Save Description
- </gr-button>
- </span>
- </fieldset>
- <h3 id="options" class$="heading-3 [[_computeHeaderClass(_options)]]">
- Group Options
- </h3>
- <fieldset>
- <section>
- <span class="title">
- Make group visible to all registered users
- </span>
- <span class="value">
- <gr-select
- id="visibleToAll"
- bind-value="{{_groupConfig.options.visible_to_all}}"
- >
- <select
- disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- >
- <template is="dom-repeat" items="[[_submitTypes]]">
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <span
- class="value"
- disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- >
- <gr-button on-click="_handleSaveOptions" disabled="[[!_options]]">
- Save Group Options
- </gr-button>
- </span>
- </fieldset>
- </fieldset>
- </div>
- </div>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
deleted file mode 100644
index e390ac5..0000000
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
+++ /dev/null
@@ -1,234 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-group.js';
-import {
- addListenerForTest,
- mockPromise,
- stubRestApi,
-} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-group');
-
-suite('gr-group tests', () => {
- let element;
-
- let groupStub;
- const group = {
- id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
- url: '#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389',
- options: {},
- description: 'Gerrit Site Administrators',
- group_id: 1,
- owner: 'Administrators',
- owner_id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
- name: 'Administrators',
- };
-
- setup(() => {
- element = basicFixture.instantiate();
- groupStub = stubRestApi('getGroupConfig').returns(Promise.resolve(group));
- });
-
- test('loading displays before group config is loaded', () => {
- assert.isTrue(element.$.loading.classList.contains('loading'));
- assert.isFalse(getComputedStyle(element.$.loading).display === 'none');
- assert.isTrue(element.$.loadedContent.classList.contains('loading'));
- assert.isTrue(getComputedStyle(element.$.loadedContent)
- .display === 'none');
- });
-
- test('default values are populated with internal group', async () => {
- stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
- element.groupId = 1;
- await element._loadGroup();
- assert.isTrue(element._groupIsInternal);
- assert.isFalse(element.$.visibleToAll.bindValue);
- });
-
- test('default values with external group', async () => {
- const groupExternal = {...group};
- groupExternal.id = 'external-group-id';
- groupStub.restore();
- groupStub = stubRestApi('getGroupConfig').returns(
- Promise.resolve(groupExternal));
- stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
- element.groupId = 1;
- await element._loadGroup();
- assert.isFalse(element._groupIsInternal);
- assert.isFalse(element.$.visibleToAll.bindValue);
- });
-
- test('rename group', async () => {
- const groupName = 'test-group';
- const groupName2 = 'test-group2';
- element.groupId = 1;
- element._groupConfig = {
- name: groupName,
- };
- element._groupName = groupName;
-
- stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
- stubRestApi('saveGroupName').returns(Promise.resolve({status: 200}));
-
- const button = element.$.inputUpdateNameBtn;
-
- await element._loadGroup();
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
-
- element.$.groupNameInput.text = groupName2;
-
- await flush();
- assert.isFalse(button.hasAttribute('disabled'));
- assert.isTrue(element.$.groupName.classList.contains('edited'));
-
- await element._handleSaveName();
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- assert.equal(element._groupName, groupName2);
- });
-
- test('rename group owner', async () => {
- const groupName = 'test-group';
- element.groupId = 1;
- element._groupConfig = {
- name: groupName,
- };
- element._groupConfigOwner = 'testId';
- element._groupOwner = true;
-
- stubRestApi('getIsGroupOwner').returns(Promise.resolve({status: 200}));
-
- const button = element.$.inputUpdateOwnerBtn;
-
- await element._loadGroup();
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
-
- element.$.groupOwnerInput.text = 'testId2';
-
- await flush();
- assert.isFalse(button.hasAttribute('disabled'));
- assert.isTrue(element.$.groupOwner.classList.contains('edited'));
-
- await element._handleSaveOwner();
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- });
-
- test('test for undefined group name', async () => {
- groupStub.restore();
-
- stubRestApi('getGroupConfig').returns(Promise.resolve({}));
-
- assert.isUndefined(element.groupId);
-
- element.groupId = 1;
-
- assert.isDefined(element.groupId);
-
- // Test that loading shows instead of filling
- // in group details
- await element._loadGroup();
- assert.isTrue(element.$.loading.classList.contains('loading'));
-
- assert.isTrue(element._loading);
- });
-
- test('test fire event', async () => {
- element._groupConfig = {
- name: 'test-group',
- };
- element.groupId = 'gg';
- stubRestApi('saveGroupName').returns(Promise.resolve({status: 200}));
-
- const showStub = sinon.stub(element, 'dispatchEvent');
- await element._handleSaveName();
- assert.isTrue(showStub.called);
- });
-
- test('_computeGroupDisabled', () => {
- let admin = true;
- let owner = false;
- let groupIsInternal = true;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), false);
-
- admin = false;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), true);
-
- owner = true;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), false);
-
- owner = false;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), true);
-
- groupIsInternal = false;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), true);
-
- admin = true;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), true);
- });
-
- test('_computeLoadingClass', () => {
- assert.equal(element._computeLoadingClass(true), 'loading');
- assert.equal(element._computeLoadingClass(false), '');
- });
-
- test('fires page-error', async () => {
- groupStub.restore();
-
- element.groupId = 1;
-
- const response = {status: 404};
- stubRestApi('getGroupConfig').callsFake((group, errFn) => {
- errFn(response);
- return Promise.resolve(undefined);
- });
-
- const promise = mockPromise();
- addListenerForTest(document, 'page-error', e => {
- assert.deepEqual(e.detail.response, response);
- promise.resolve();
- });
-
- element._loadGroup();
- await promise;
- });
-
- test('uuid', () => {
- element._groupConfig = {
- id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
- };
-
- assert.equal(element._groupConfig.id, element.$.uuid.text);
-
- element._groupConfig = {
- id: 'user%2Fgroup',
- };
-
- assert.equal('user/group', element.$.uuid.text);
- });
-});
-
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.ts b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.ts
new file mode 100644
index 0000000..c2ef76a
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.ts
@@ -0,0 +1,320 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-group';
+import {GrGroup} from './gr-group';
+import {
+ addListenerForTest,
+ mockPromise,
+ queryAndAssert,
+ stubRestApi,
+} from '../../../test/test-utils';
+import {createGroupInfo} from '../../../test/test-data-generators.js';
+import {GroupId, GroupInfo, GroupName} from '../../../types/common';
+import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {GrCopyClipboard} from '../../shared/gr-copy-clipboard/gr-copy-clipboard';
+import {GrSelect} from '../../shared/gr-select/gr-select';
+
+const basicFixture = fixtureFromElement('gr-group');
+
+suite('gr-group tests', () => {
+ let element: GrGroup;
+ let groupStub: sinon.SinonStub;
+
+ const group: GroupInfo = {
+ ...createGroupInfo('6a1e70e1a88782771a91808c8af9bbb7a9871389'),
+ url: '#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389',
+ options: {
+ visible_to_all: false,
+ },
+ description: 'Gerrit Site Administrators',
+ group_id: 1,
+ owner: 'Administrators',
+ owner_id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
+ name: 'Administrators' as GroupName,
+ };
+
+ setup(async () => {
+ element = basicFixture.instantiate();
+ await element.updateComplete;
+ groupStub = stubRestApi('getGroupConfig').returns(Promise.resolve(group));
+ });
+
+ test('loading displays before group config is loaded', () => {
+ assert.isTrue(
+ queryAndAssert<HTMLDivElement>(element, '#loading').classList.contains(
+ 'loading'
+ )
+ );
+ assert.isFalse(
+ getComputedStyle(queryAndAssert<HTMLDivElement>(element, '#loading'))
+ .display === 'none'
+ );
+ assert.isTrue(
+ queryAndAssert<HTMLDivElement>(
+ element,
+ '#loadedContent'
+ ).classList.contains('loading')
+ );
+ assert.isTrue(
+ getComputedStyle(
+ queryAndAssert<HTMLDivElement>(element, '#loadedContent')
+ ).display === 'none'
+ );
+ });
+
+ test('default values are populated with internal group', async () => {
+ stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
+ element.groupId = '1' as GroupId;
+ await element.loadGroup();
+ assert.isTrue(element.groupIsInternal);
+ // The value returned is a boolean in a string
+ // thus we have to check with the string.
+ assert.equal(
+ queryAndAssert<GrSelect>(element, '#visibleToAll').bindValue,
+ 'false'
+ );
+ });
+
+ test('default values with external group', async () => {
+ const groupExternal = {...group};
+ groupExternal.id = 'external-group-id' as GroupId;
+ groupStub.restore();
+ groupStub = stubRestApi('getGroupConfig').returns(
+ Promise.resolve(groupExternal)
+ );
+ stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
+ element.groupId = '1' as GroupId;
+ await element.loadGroup();
+ assert.isFalse(element.groupIsInternal);
+ // The value returned is a boolean in a string
+ // thus we have to check with the string.
+ assert.equal(
+ queryAndAssert<GrSelect>(element, '#visibleToAll').bindValue,
+ 'false'
+ );
+ });
+
+ test('rename group', async () => {
+ const groupName = 'test-group';
+ const groupName2 = 'test-group2';
+ element.groupId = '1' as GroupId;
+ element.groupConfig = {
+ name: groupName as GroupName,
+ id: '1' as GroupId,
+ };
+ element.originalName = groupName as GroupName;
+
+ stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
+ stubRestApi('saveGroupName').returns(
+ Promise.resolve({...new Response(), status: 200})
+ );
+
+ const button = queryAndAssert<GrButton>(element, '#inputUpdateNameBtn');
+
+ await element.loadGroup();
+ assert.isTrue(button.hasAttribute('disabled'));
+ assert.isFalse(
+ queryAndAssert<HTMLHeadingElement>(element, '#Title').classList.contains(
+ 'edited'
+ )
+ );
+
+ queryAndAssert<GrAutocomplete>(element, '#groupNameInput').text =
+ groupName2;
+
+ await element.updateComplete;
+
+ assert.isFalse(button.hasAttribute('disabled'));
+ assert.isTrue(
+ queryAndAssert<HTMLHeadingElement>(
+ element,
+ '#groupName'
+ ).classList.contains('edited')
+ );
+
+ await element.handleSaveName();
+ assert.isTrue(button.disabled);
+ assert.isFalse(
+ queryAndAssert<HTMLHeadingElement>(element, '#Title').classList.contains(
+ 'edited'
+ )
+ );
+ assert.equal(element.originalName, groupName2);
+ });
+
+ test('rename group owner', async () => {
+ const groupName = 'test-group';
+ element.groupId = '1' as GroupId;
+ element.groupConfig = {
+ name: groupName as GroupName,
+ id: '1' as GroupId,
+ };
+ element.groupConfigOwner = 'testId';
+ element.groupOwner = true;
+
+ stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
+
+ const button = queryAndAssert<GrButton>(element, '#inputUpdateOwnerBtn');
+
+ await element.loadGroup();
+ assert.isTrue(button.disabled);
+ assert.isFalse(
+ queryAndAssert<HTMLHeadingElement>(element, '#Title').classList.contains(
+ 'edited'
+ )
+ );
+
+ queryAndAssert<GrAutocomplete>(element, '#groupOwnerInput').text =
+ 'testId2';
+
+ await element.updateComplete;
+ assert.isFalse(button.disabled);
+ assert.isTrue(
+ queryAndAssert<HTMLHeadingElement>(
+ element,
+ '#groupOwner'
+ ).classList.contains('edited')
+ );
+
+ await element.handleSaveOwner();
+ assert.isTrue(button.disabled);
+ assert.isFalse(
+ queryAndAssert<HTMLHeadingElement>(element, '#Title').classList.contains(
+ 'edited'
+ )
+ );
+ });
+
+ test('test for undefined group name', async () => {
+ groupStub.restore();
+
+ stubRestApi('getGroupConfig').returns(Promise.resolve(undefined));
+
+ assert.isUndefined(element.groupId);
+
+ element.groupId = '1' as GroupId;
+
+ assert.isDefined(element.groupId);
+
+ // Test that loading shows instead of filling
+ // in group details
+ await element.loadGroup();
+ assert.isTrue(
+ queryAndAssert<HTMLDivElement>(element, '#loading').classList.contains(
+ 'loading'
+ )
+ );
+
+ assert.isTrue(element.loading);
+ });
+
+ test('test fire event', async () => {
+ element.groupConfig = {
+ name: 'test-group' as GroupName,
+ id: '1' as GroupId,
+ };
+ element.groupId = 'gg' as GroupId;
+ stubRestApi('saveGroupName').returns(
+ Promise.resolve({...new Response(), status: 200})
+ );
+
+ const showStub = sinon.stub(element, 'dispatchEvent');
+ await element.handleSaveName();
+ assert.isTrue(showStub.called);
+ });
+
+ test('computeGroupDisabled', () => {
+ element.isAdmin = true;
+ element.groupOwner = false;
+ element.groupIsInternal = true;
+ assert.equal(element.computeGroupDisabled(), false);
+
+ element.isAdmin = false;
+ assert.equal(element.computeGroupDisabled(), true);
+
+ element.groupOwner = true;
+ assert.equal(element.computeGroupDisabled(), false);
+
+ element.groupOwner = false;
+ assert.equal(element.computeGroupDisabled(), true);
+
+ element.groupIsInternal = false;
+ assert.equal(element.computeGroupDisabled(), true);
+
+ element.isAdmin = true;
+ assert.equal(element.computeGroupDisabled(), true);
+ });
+
+ test('computeLoadingClass', () => {
+ element.loading = true;
+ assert.equal(element.computeLoadingClass(), 'loading');
+ element.loading = false;
+ assert.equal(element.computeLoadingClass(), '');
+ });
+
+ test('fires page-error', async () => {
+ groupStub.restore();
+
+ element.groupId = '1' as GroupId;
+
+ const response = {...new Response(), status: 404};
+ stubRestApi('getGroupConfig').callsFake((_, errFn) => {
+ if (errFn !== undefined) {
+ errFn(response);
+ } else {
+ assert.fail('errFn is undefined');
+ }
+ return Promise.resolve(undefined);
+ });
+
+ const promise = mockPromise();
+ addListenerForTest(document, 'page-error', e => {
+ assert.deepEqual((e as CustomEvent).detail.response, response);
+ promise.resolve();
+ });
+
+ await element.loadGroup();
+ await promise;
+ });
+
+ test('uuid', async () => {
+ element.groupConfig = {
+ id: '6a1e70e1a88782771a91808c8af9bbb7a9871389' as GroupId,
+ };
+
+ await element.updateComplete;
+
+ assert.equal(
+ element.groupConfig.id,
+ queryAndAssert<GrCopyClipboard>(element, '#uuid').text
+ );
+
+ element.groupConfig = {
+ id: 'user%2Fgroup' as GroupId,
+ };
+
+ await element.updateComplete;
+
+ assert.equal(
+ 'user/group',
+ queryAndAssert<GrCopyClipboard>(element, '#uuid').text
+ );
+ });
+});
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 f2b84c2..c953be4 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts
@@ -18,6 +18,7 @@
import '@polymer/paper-toggle-button/paper-toggle-button';
import '../../../styles/gr-form-styles';
import '../../../styles/gr-menu-page-styles';
+import '../../../styles/gr-paper-styles';
import '../../../styles/shared-styles';
import '../../shared/gr-autocomplete/gr-autocomplete';
import '../../shared/gr-button/gr-button';
@@ -52,8 +53,9 @@
EditableProjectAccessGroups,
} from '../gr-repo-access/gr-repo-access-interfaces';
import {PolymerDomRepeatEvent} from '../../../types/types';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {fireEvent} from '../../../utils/event-util';
+import {PolymerDomRepeatCustomEvent} from '../../../types/types';
const MAX_AUTOCOMPLETE_RESULTS = 20;
@@ -140,7 +142,7 @@
@property({type: Boolean})
_originalExclusiveValue?: boolean;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
@@ -411,6 +413,19 @@
_onTapExclusiveToggle(e: Event) {
e.preventDefault();
}
+
+ _handleRuleChanged(e: PolymerDomRepeatCustomEvent) {
+ if (
+ this._rules === undefined ||
+ (e as CustomEvent).detail.value === undefined
+ )
+ return;
+ const index = Number(e.model.index);
+ if (isNaN(index)) {
+ return;
+ }
+ this.splice('_rules', index, (e as CustomEvent).detail.value);
+ }
}
declare global {
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.ts b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.ts
index 3559194..eacd5ef 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.ts
@@ -70,6 +70,9 @@
display: block;
}
</style>
+ <style include="gr-paper-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="gr-form-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
@@ -112,9 +115,10 @@
group-id="[[rule.id]]"
group-name="[[_computeGroupName(groups, rule.id)]]"
permission="[[permission.id]]"
- rule="{{rule}}"
+ rule="[[rule]]"
section="[[section]]"
on-added-rule-removed="_handleAddedRuleRemoved"
+ on-rule-changed="_handleRuleChanged"
></gr-rule-editor>
</template>
<div id="addRule">
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.js
deleted file mode 100644
index 8f25db5..0000000
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.js
+++ /dev/null
@@ -1,409 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-permission.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-permission');
-
-suite('gr-permission tests', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- stubRestApi('getSuggestedGroups').returns(
- Promise.resolve({
- 'Administrators': {
- id: '4c97682e6ce61b7247f3381b6f1789356666de7f',
- },
- 'Anonymous Users': {
- id: 'global%3AAnonymous-Users',
- },
- }));
- });
-
- suite('unit tests', () => {
- test('_sortPermission', () => {
- const permission = {
- id: 'submit',
- value: {
- rules: {
- 'global:Project-Owners': {
- action: 'ALLOW',
- force: false,
- },
- '4c97682e6ce6b7247f3381b6f1789356666de7f': {
- action: 'ALLOW',
- force: false,
- },
- },
- },
- };
-
- const expectedRules = [
- {
- id: '4c97682e6ce6b7247f3381b6f1789356666de7f',
- value: {action: 'ALLOW', force: false},
- },
- {
- id: 'global:Project-Owners',
- value: {action: 'ALLOW', force: false},
- },
- ];
-
- element._sortPermission(permission);
- assert.deepEqual(element._rules, expectedRules);
- });
-
- test('_computeLabel and _computeLabelValues', () => {
- const labels = {
- 'Code-Review': {
- default_value: 0,
- values: {
- ' 0': 'No score',
- '-1': 'I would prefer this is not merged as is',
- '-2': 'This shall not be merged',
- '+1': 'Looks good to me, but someone else must approve',
- '+2': 'Looks good to me, approved',
- },
- },
- };
- let permission = {
- id: 'label-Code-Review',
- value: {
- label: 'Code-Review',
- rules: {
- 'global:Project-Owners': {
- action: 'ALLOW',
- force: false,
- min: -2,
- max: 2,
- },
- '4c97682e6ce6b7247f3381b6f1789356666de7f': {
- action: 'ALLOW',
- force: false,
- min: -2,
- max: 2,
- },
- },
- },
- };
-
- const expectedLabelValues = [
- {value: -2, text: 'This shall not be merged'},
- {value: -1, text: 'I would prefer this is not merged as is'},
- {value: 0, text: 'No score'},
- {value: 1, text: 'Looks good to me, but someone else must approve'},
- {value: 2, text: 'Looks good to me, approved'},
- ];
-
- const expectedLabel = {
- name: 'Code-Review',
- values: expectedLabelValues,
- };
-
- assert.deepEqual(element._computeLabelValues(
- labels['Code-Review'].values), expectedLabelValues);
-
- assert.deepEqual(element._computeLabel(permission, labels),
- expectedLabel);
-
- permission = {
- id: 'label-reviewDB',
- value: {
- label: 'reviewDB',
- rules: {
- 'global:Project-Owners': {
- action: 'ALLOW',
- force: false,
- },
- '4c97682e6ce6b7247f3381b6f1789356666de7f': {
- action: 'ALLOW',
- force: false,
- },
- },
- },
- };
-
- assert.isNotOk(element._computeLabel(permission, labels));
- });
-
- test('_computeSectionClass', () => {
- let deleted = true;
- let editing = false;
- assert.equal(element._computeSectionClass(editing, deleted), 'deleted');
-
- deleted = false;
- assert.equal(element._computeSectionClass(editing, deleted), '');
-
- editing = true;
- assert.equal(element._computeSectionClass(editing, deleted), 'editing');
-
- deleted = true;
- assert.equal(element._computeSectionClass(editing, deleted),
- 'editing deleted');
- });
-
- test('_computeGroupName', () => {
- const groups = {
- abc123: {name: 'test group'},
- bcd234: {},
- };
- assert.equal(element._computeGroupName(groups, 'abc123'), 'test group');
- assert.equal(element._computeGroupName(groups, 'bcd234'), 'bcd234');
- });
-
- test('_computeGroupsWithRules', () => {
- const rules = [
- {
- id: '4c97682e6ce6b7247f3381b6f1789356666de7f',
- value: {action: 'ALLOW', force: false},
- },
- {
- id: 'global:Project-Owners',
- value: {action: 'ALLOW', force: false},
- },
- ];
- const groupsWithRules = {
- '4c97682e6ce6b7247f3381b6f1789356666de7f': true,
- 'global:Project-Owners': true,
- };
- assert.deepEqual(element._computeGroupsWithRules(rules),
- groupsWithRules);
- });
-
- test('_getGroupSuggestions without existing rules', async () => {
- element._groupsWithRules = {};
-
- const groups = await element._getGroupSuggestions();
- assert.deepEqual(groups, [
- {
- name: 'Administrators',
- value: '4c97682e6ce61b7247f3381b6f1789356666de7f',
- }, {
- name: 'Anonymous Users',
- value: 'global%3AAnonymous-Users',
- },
- ]);
- });
-
- test('_getGroupSuggestions with existing rules filters them', async () => {
- element._groupsWithRules = {
- '4c97682e6ce61b7247f3381b6f1789356666de7f': true,
- };
-
- const groups = await element._getGroupSuggestions();
- assert.deepEqual(groups, [{
- name: 'Anonymous Users',
- value: 'global%3AAnonymous-Users',
- }]);
- });
-
- test('_handleRemovePermission', () => {
- element.editing = true;
- element.permission = {value: {rules: {}}};
- element._handleRemovePermission();
- assert.isTrue(element._deleted);
- assert.isTrue(element.permission.value.deleted);
-
- element.editing = false;
- assert.isFalse(element._deleted);
- assert.isNotOk(element.permission.value.deleted);
- });
-
- test('_handleUndoRemove', () => {
- element.permission = {value: {deleted: true, rules: {}}};
- element._handleUndoRemove();
- assert.isFalse(element._deleted);
- assert.isNotOk(element.permission.value.deleted);
- });
-
- test('_computeHasRange', () => {
- assert.isTrue(element._computeHasRange('Query Limit'));
-
- assert.isTrue(element._computeHasRange('Batch Changes Limit'));
-
- assert.isFalse(element._computeHasRange('test'));
- });
- });
-
- suite('interactions', () => {
- setup(() => {
- sinon.spy(element, '_computeLabel');
- element.name = 'Priority';
- element.section = 'refs/*';
- element.labels = {
- 'Code-Review': {
- values: {
- ' 0': 'No score',
- '-1': 'I would prefer this is not merged as is',
- '-2': 'This shall not be merged',
- '+1': 'Looks good to me, but someone else must approve',
- '+2': 'Looks good to me, approved',
- },
- default_value: 0,
- },
- };
- element.permission = {
- id: 'label-Code-Review',
- value: {
- label: 'Code-Review',
- rules: {
- 'global:Project-Owners': {
- action: 'ALLOW',
- force: false,
- min: -2,
- max: 2,
- },
- '4c97682e6ce6b7247f3381b6f1789356666de7f': {
- action: 'ALLOW',
- force: false,
- min: -2,
- max: 2,
- },
- },
- },
- };
- element._setupValues();
- flush();
- });
-
- test('adding a rule', () => {
- element.name = 'Priority';
- element.section = 'refs/*';
- element.groups = {};
- element.$.groupAutocomplete.text = 'ldap/tests te.st';
- const e = {
- detail: {
- value: 'ldap:CN=test+te.st',
- },
- };
- element.editing = true;
- assert.equal(element._rules.length, 2);
- assert.equal(Object.keys(element._groupsWithRules).length, 2);
- element._handleAddRuleItem(e);
- flush();
- assert.deepEqual(element.groups, {'ldap:CN=test te.st': {
- name: 'ldap/tests te.st'}});
- assert.equal(element._rules.length, 3);
- assert.equal(Object.keys(element._groupsWithRules).length, 3);
- assert.deepEqual(element.permission.value.rules['ldap:CN=test te.st'],
- {action: 'ALLOW', min: -2, max: 2, added: true});
- assert.equal(element.$.groupAutocomplete.text, '');
- // New rule should be removed if cancel from editing.
- element.editing = false;
- assert.equal(element._rules.length, 2);
- assert.equal(Object.keys(element.permission.value.rules).length, 2);
- });
-
- test('removing an added rule', () => {
- element.name = 'Priority';
- element.section = 'refs/*';
- element.groups = {};
- element.$.groupAutocomplete.text = 'new group name';
- assert.equal(element._rules.length, 2);
- element.shadowRoot
- .querySelector('gr-rule-editor').dispatchEvent(
- new CustomEvent('added-rule-removed', {
- composed: true, bubbles: true,
- }));
- flush();
- assert.equal(element._rules.length, 1);
- });
-
- test('removing an added permission', () => {
- const removeStub = sinon.stub();
- element.addEventListener('added-permission-removed', removeStub);
- element.editing = true;
- element.name = 'Priority';
- element.section = 'refs/*';
- element.permission.value.added = true;
- MockInteractions.tap(element.$.removeBtn);
- assert.isTrue(removeStub.called);
- });
-
- test('removing the permission', () => {
- element.editing = true;
- element.name = 'Priority';
- element.section = 'refs/*';
-
- const removeStub = sinon.stub();
- element.addEventListener('added-permission-removed', removeStub);
-
- assert.isFalse(element.$.permission.classList.contains('deleted'));
- assert.isFalse(element._deleted);
- MockInteractions.tap(element.$.removeBtn);
- assert.isTrue(element.$.permission.classList.contains('deleted'));
- assert.isTrue(element._deleted);
- MockInteractions.tap(element.$.undoRemoveBtn);
- assert.isFalse(element.$.permission.classList.contains('deleted'));
- assert.isFalse(element._deleted);
- assert.isFalse(removeStub.called);
- });
-
- test('modify a permission', () => {
- element.editing = true;
- element.name = 'Priority';
- element.section = 'refs/*';
-
- assert.isFalse(element._originalExclusiveValue);
- assert.isNotOk(element.permission.value.modified);
- MockInteractions.tap(element.shadowRoot
- .querySelector('#exclusiveToggle'));
- flush();
- assert.isTrue(element.permission.value.exclusive);
- assert.isTrue(element.permission.value.modified);
- assert.isFalse(element._originalExclusiveValue);
- element.editing = false;
- assert.isFalse(element.permission.value.exclusive);
- });
-
- test('_handleValueChange', () => {
- const modifiedHandler = sinon.stub();
- element.permission = {value: {rules: {}}};
- element.addEventListener('access-modified', modifiedHandler);
- assert.isNotOk(element.permission.value.modified);
- element._handleValueChange();
- assert.isTrue(element.permission.value.modified);
- assert.isTrue(modifiedHandler.called);
- });
-
- test('Exclusive hidden for owner permission', () => {
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('#exclusiveToggle')).display,
- 'flex');
- element.set(['permission', 'id'], 'owner');
- flush();
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('#exclusiveToggle')).display,
- 'none');
- });
-
- test('Exclusive hidden for any global permissions', () => {
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('#exclusiveToggle')).display,
- 'flex');
- element.section = 'GLOBAL_CAPABILITIES';
- flush();
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('#exclusiveToggle')).display,
- 'none');
- });
- });
-});
-
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.ts b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.ts
new file mode 100644
index 0000000..fac0729
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.ts
@@ -0,0 +1,465 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-permission';
+import {GrPermission} from './gr-permission';
+import {stubRestApi} from '../../../test/test-utils';
+import {GitRef, GroupId, GroupName} from '../../../types/common';
+import {PermissionAction} from '../../../constants/constants';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+import {
+ AutocompleteCommitEventDetail,
+ GrAutocomplete,
+} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {queryAndAssert} from '../../../test/test-utils';
+import {GrRuleEditor} from '../gr-rule-editor/gr-rule-editor';
+import {GrButton} from '../../shared/gr-button/gr-button';
+
+const basicFixture = fixtureFromElement('gr-permission');
+
+suite('gr-permission tests', () => {
+ let element: GrPermission;
+
+ setup(() => {
+ element = basicFixture.instantiate();
+ stubRestApi('getSuggestedGroups').returns(
+ Promise.resolve({
+ Administrators: {
+ id: '4c97682e6ce61b7247f3381b6f1789356666de7f' as GroupId,
+ },
+ 'Anonymous Users': {
+ id: 'global%3AAnonymous-Users' as GroupId,
+ },
+ })
+ );
+ });
+
+ suite('unit tests', () => {
+ test('_sortPermission', () => {
+ const permission = {
+ id: 'submit' as GitRef,
+ value: {
+ rules: {
+ 'global:Project-Owners': {
+ action: PermissionAction.ALLOW,
+ force: false,
+ },
+ '4c97682e6ce6b7247f3381b6f1789356666de7f': {
+ action: PermissionAction.ALLOW,
+ force: false,
+ },
+ },
+ },
+ };
+
+ const expectedRules = [
+ {
+ id: '4c97682e6ce6b7247f3381b6f1789356666de7f' as GitRef,
+ value: {action: PermissionAction.ALLOW, force: false},
+ },
+ {
+ id: 'global:Project-Owners' as GitRef,
+ value: {action: PermissionAction.ALLOW, force: false},
+ },
+ ];
+
+ element._sortPermission(permission);
+ assert.deepEqual(element._rules, expectedRules);
+ });
+
+ test('_computeLabel and _computeLabelValues', () => {
+ const labels = {
+ 'Code-Review': {
+ default_value: 0,
+ values: {
+ ' 0': 'No score',
+ '-1': 'I would prefer this is not merged as is',
+ '-2': 'This shall not be merged',
+ '+1': 'Looks good to me, but someone else must approve',
+ '+2': 'Looks good to me, approved',
+ },
+ },
+ };
+ let permission = {
+ id: 'label-Code-Review' as GitRef,
+ value: {
+ label: 'Code-Review',
+ rules: {
+ 'global:Project-Owners': {
+ action: PermissionAction.ALLOW,
+ force: false,
+ min: -2,
+ max: 2,
+ },
+ '4c97682e6ce6b7247f3381b6f1789356666de7f': {
+ action: PermissionAction.ALLOW,
+ force: false,
+ min: -2,
+ max: 2,
+ },
+ },
+ },
+ };
+
+ const expectedLabelValues = [
+ {value: -2, text: 'This shall not be merged'},
+ {value: -1, text: 'I would prefer this is not merged as is'},
+ {value: 0, text: 'No score'},
+ {value: 1, text: 'Looks good to me, but someone else must approve'},
+ {value: 2, text: 'Looks good to me, approved'},
+ ];
+
+ const expectedLabel = {
+ name: 'Code-Review',
+ values: expectedLabelValues,
+ };
+
+ assert.deepEqual(
+ element._computeLabelValues(labels['Code-Review'].values),
+ expectedLabelValues
+ );
+
+ assert.deepEqual(
+ element._computeLabel(permission, labels),
+ expectedLabel
+ );
+
+ permission = {
+ id: 'label-reviewDB' as GitRef,
+ value: {
+ label: 'reviewDB',
+ rules: {
+ 'global:Project-Owners': {
+ action: PermissionAction.ALLOW,
+ force: false,
+ min: 0,
+ max: 0,
+ },
+ '4c97682e6ce6b7247f3381b6f1789356666de7f': {
+ action: PermissionAction.ALLOW,
+ force: false,
+ min: 0,
+ max: 0,
+ },
+ },
+ },
+ };
+
+ assert.isNotOk(element._computeLabel(permission, labels));
+ });
+
+ test('_computeSectionClass', () => {
+ let deleted = true;
+ let editing = false;
+ assert.equal(element._computeSectionClass(editing, deleted), 'deleted');
+
+ deleted = false;
+ assert.equal(element._computeSectionClass(editing, deleted), '');
+
+ editing = true;
+ assert.equal(element._computeSectionClass(editing, deleted), 'editing');
+
+ deleted = true;
+ assert.equal(
+ element._computeSectionClass(editing, deleted),
+ 'editing deleted'
+ );
+ });
+
+ test('_computeGroupName', () => {
+ const groups = {
+ abc123: {id: '1' as GroupId, name: 'test group' as GroupName},
+ bcd234: {id: '1' as GroupId},
+ };
+ assert.equal(
+ element._computeGroupName(groups, 'abc123' as GroupId),
+ 'test group' as GroupName
+ );
+ assert.equal(
+ element._computeGroupName(groups, 'bcd234' as GroupId),
+ 'bcd234' as GroupName
+ );
+ });
+
+ test('_computeGroupsWithRules', () => {
+ const rules = [
+ {
+ id: '4c97682e6ce6b7247f3381b6f1789356666de7f' as GitRef,
+ value: {action: PermissionAction.ALLOW, force: false},
+ },
+ {
+ id: 'global:Project-Owners' as GitRef,
+ value: {action: PermissionAction.ALLOW, force: false},
+ },
+ ];
+ const groupsWithRules = {
+ '4c97682e6ce6b7247f3381b6f1789356666de7f': true,
+ 'global:Project-Owners': true,
+ };
+ assert.deepEqual(element._computeGroupsWithRules(rules), groupsWithRules);
+ });
+
+ test('_getGroupSuggestions without existing rules', async () => {
+ element._groupsWithRules = {};
+
+ const groups = await element._getGroupSuggestions();
+ assert.deepEqual(groups, [
+ {
+ name: 'Administrators',
+ value: '4c97682e6ce61b7247f3381b6f1789356666de7f',
+ },
+ {
+ name: 'Anonymous Users',
+ value: 'global%3AAnonymous-Users',
+ },
+ ]);
+ });
+
+ test('_getGroupSuggestions with existing rules filters them', async () => {
+ element._groupsWithRules = {
+ '4c97682e6ce61b7247f3381b6f1789356666de7f': true,
+ };
+
+ const groups = await element._getGroupSuggestions();
+ assert.deepEqual(groups, [
+ {
+ name: 'Anonymous Users',
+ value: 'global%3AAnonymous-Users',
+ },
+ ]);
+ });
+
+ test('_handleRemovePermission', () => {
+ element.editing = true;
+ element.permission = {id: 'test' as GitRef, value: {rules: {}}};
+ element._handleRemovePermission();
+ assert.isTrue(element._deleted);
+ assert.isTrue(element.permission.value.deleted);
+
+ element.editing = false;
+ assert.isFalse(element._deleted);
+ assert.isNotOk(element.permission.value.deleted);
+ });
+
+ test('_handleUndoRemove', () => {
+ element.permission = {
+ id: 'test' as GitRef,
+ value: {deleted: true, rules: {}},
+ };
+ element._handleUndoRemove();
+ assert.isFalse(element._deleted);
+ assert.isNotOk(element.permission.value.deleted);
+ });
+
+ test('_computeHasRange', () => {
+ assert.isTrue(element._computeHasRange('Query Limit'));
+
+ assert.isTrue(element._computeHasRange('Batch Changes Limit'));
+
+ assert.isFalse(element._computeHasRange('test'));
+ });
+ });
+
+ suite('interactions', () => {
+ setup(() => {
+ sinon.spy(element, '_computeLabel');
+ element.name = 'Priority';
+ element.section = 'refs/*' as GitRef;
+ element.labels = {
+ 'Code-Review': {
+ values: {
+ ' 0': 'No score',
+ '-1': 'I would prefer this is not merged as is',
+ '-2': 'This shall not be merged',
+ '+1': 'Looks good to me, but someone else must approve',
+ '+2': 'Looks good to me, approved',
+ },
+ default_value: 0,
+ },
+ };
+ element.permission = {
+ id: 'label-Code-Review' as GitRef,
+ value: {
+ label: 'Code-Review',
+ rules: {
+ 'global:Project-Owners': {
+ action: PermissionAction.ALLOW,
+ force: false,
+ min: -2,
+ max: 2,
+ },
+ '4c97682e6ce6b7247f3381b6f1789356666de7f': {
+ action: PermissionAction.ALLOW,
+ force: false,
+ min: -2,
+ max: 2,
+ },
+ },
+ },
+ };
+ element._setupValues();
+ flush();
+ });
+
+ test('adding a rule', () => {
+ element.name = 'Priority';
+ element.section = 'refs/*' as GitRef;
+ element.groups = {};
+ queryAndAssert<GrAutocomplete>(element, '#groupAutocomplete').text =
+ 'ldap/tests te.st';
+ const e = {
+ detail: {
+ value: 'ldap:CN=test+te.st',
+ },
+ } as CustomEvent<AutocompleteCommitEventDetail>;
+ element.editing = true;
+ assert.equal(element._rules!.length, 2);
+ assert.equal(Object.keys(element._groupsWithRules!).length, 2);
+ element._handleAddRuleItem(e);
+ flush();
+ assert.deepEqual(element.groups, {
+ 'ldap:CN=test te.st': {
+ name: 'ldap/tests te.st',
+ },
+ });
+ assert.equal(element._rules!.length, 3);
+ assert.equal(Object.keys(element._groupsWithRules!).length, 3);
+ assert.deepEqual(element.permission!.value.rules['ldap:CN=test te.st'], {
+ action: PermissionAction.ALLOW,
+ min: -2,
+ max: 2,
+ added: true,
+ });
+ assert.equal(
+ queryAndAssert<GrAutocomplete>(element, '#groupAutocomplete').text,
+ ''
+ );
+ // New rule should be removed if cancel from editing.
+ element.editing = false;
+ assert.equal(element._rules!.length, 2);
+ assert.equal(Object.keys(element.permission!.value.rules).length, 2);
+ });
+
+ test('removing an added rule', async () => {
+ element.name = 'Priority';
+ element.section = 'refs/*' as GitRef;
+ element.groups = {};
+ queryAndAssert<GrAutocomplete>(element, '#groupAutocomplete').text =
+ 'new group name';
+ assert.equal(element._rules!.length, 2);
+ queryAndAssert<GrRuleEditor>(element, 'gr-rule-editor').dispatchEvent(
+ new CustomEvent('added-rule-removed', {
+ composed: true,
+ bubbles: true,
+ })
+ );
+ await flush();
+ assert.equal(element._rules!.length, 1);
+ });
+
+ test('removing an added permission', () => {
+ const removeStub = sinon.stub();
+ element.addEventListener('added-permission-removed', removeStub);
+ element.editing = true;
+ element.name = 'Priority';
+ element.section = 'refs/*' as GitRef;
+ element.permission!.value.added = true;
+ MockInteractions.tap(queryAndAssert<GrButton>(element, '#removeBtn'));
+ assert.isTrue(removeStub.called);
+ });
+
+ test('removing the permission', () => {
+ element.editing = true;
+ element.name = 'Priority';
+ element.section = 'refs/*' as GitRef;
+
+ const removeStub = sinon.stub();
+ element.addEventListener('added-permission-removed', removeStub);
+
+ assert.isFalse(
+ queryAndAssert(element, '#permission').classList.contains('deleted')
+ );
+ assert.isFalse(element._deleted);
+ MockInteractions.tap(queryAndAssert<GrButton>(element, '#removeBtn'));
+ assert.isTrue(
+ queryAndAssert(element, '#permission').classList.contains('deleted')
+ );
+ assert.isTrue(element._deleted);
+ MockInteractions.tap(queryAndAssert<GrButton>(element, '#undoRemoveBtn'));
+ assert.isFalse(
+ queryAndAssert(element, '#permission').classList.contains('deleted')
+ );
+ assert.isFalse(element._deleted);
+ assert.isFalse(removeStub.called);
+ });
+
+ test('modify a permission', () => {
+ element.editing = true;
+ element.name = 'Priority';
+ element.section = 'refs/*' as GitRef;
+
+ assert.isFalse(element._originalExclusiveValue);
+ assert.isNotOk(element.permission!.value.modified);
+ queryAndAssert(element, '#exclusiveToggle');
+ MockInteractions.tap(queryAndAssert(element, '#exclusiveToggle'));
+ flush();
+ assert.isTrue(element.permission!.value.exclusive);
+ assert.isTrue(element.permission!.value.modified);
+ assert.isFalse(element._originalExclusiveValue);
+ element.editing = false;
+ assert.isFalse(element.permission!.value.exclusive);
+ });
+
+ test('_handleValueChange', () => {
+ const modifiedHandler = sinon.stub();
+ element.permission = {id: '0' as GitRef, value: {rules: {}}};
+ element.addEventListener('access-modified', modifiedHandler);
+ assert.isNotOk(element.permission.value.modified);
+ element._handleValueChange();
+ assert.isTrue(element.permission.value.modified);
+ assert.isTrue(modifiedHandler.called);
+ });
+
+ test('Exclusive hidden for owner permission', () => {
+ queryAndAssert(element, '#exclusiveToggle');
+ assert.equal(
+ getComputedStyle(queryAndAssert(element, '#exclusiveToggle')).display,
+ 'flex'
+ );
+ element.set(['permission', 'id'], 'owner');
+ flush();
+ assert.equal(
+ getComputedStyle(queryAndAssert(element, '#exclusiveToggle')).display,
+ 'none'
+ );
+ });
+
+ test('Exclusive hidden for any global permissions', () => {
+ assert.equal(
+ getComputedStyle(queryAndAssert(element, '#exclusiveToggle')).display,
+ 'flex'
+ );
+ element.section = 'GLOBAL_CAPABILITIES' as GitRef;
+ flush();
+ assert.equal(
+ getComputedStyle(queryAndAssert(element, '#exclusiveToggle')).display,
+ 'none'
+ );
+ });
+ });
+});
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
index 9f51688..3393596 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
@@ -14,80 +14,160 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/gr-table-styles';
-import '../../../styles/shared-styles';
+
import '../../shared/gr-list-view/gr-list-view';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-plugin-list_html';
-import {customElement, property} from '@polymer/decorators';
import {PluginInfo} from '../../../types/common';
import {firePageError, fireTitleChange} from '../../../utils/event-util';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {ErrorCallback} from '../../../api/rest';
import {encodeURL, getBaseUrl} from '../../../utils/url-util';
import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
-import {ListViewParams} from '../../gr-app-types';
+import {AppElementAdminParams} from '../../gr-app-types';
+import {tableStyles} from '../../../styles/gr-table-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, css, html} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
-interface PluginInfoWithName extends PluginInfo {
+// Exported for tests
+export interface PluginInfoWithName extends PluginInfo {
name: string;
}
@customElement('gr-plugin-list')
-export class GrPluginList extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
+export class GrPluginList extends LitElement {
+ readonly path = '/admin/plugins';
/**
* URL params passed from the router.
*/
- @property({type: Object, observer: '_paramsChanged'})
- params?: ListViewParams;
+ @property({type: Object})
+ params?: AppElementAdminParams;
/**
* Offset of currently visible query results.
*/
- @property({type: Number})
- _offset = 0;
+ @state() private offset = 0;
- @property({type: String})
- readonly _path = '/admin/plugins';
+ // private but used in test
+ @state() plugins?: PluginInfoWithName[];
- @property({type: Array})
- _plugins?: PluginInfoWithName[];
+ @state() private pluginsPerPage = 25;
- /**
- * Because we request one more than the pluginsPerPage, _shownPlugins
- * maybe one less than _plugins.
- **/
- @property({type: Array, computed: 'computeShownItems(_plugins)'})
- _shownPlugins?: PluginInfoWithName[];
+ // private but used in test
+ @state() loading = true;
- @property({type: Number})
- _pluginsPerPage = 25;
+ @state() private filter = '';
- @property({type: Boolean})
- _loading = true;
-
- @property({type: String})
- _filter = '';
-
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
override connectedCallback() {
super.connectedCallback();
fireTitleChange(this, 'Plugins');
}
- _paramsChanged(params: ListViewParams) {
- this._loading = true;
- this._filter = params?.filter ?? '';
- this._offset = Number(params?.offset ?? 0);
-
- return this._getPlugins(this._filter, this._pluginsPerPage, this._offset);
+ static override get styles() {
+ return [
+ tableStyles,
+ sharedStyles,
+ css`
+ .placeholder {
+ color: var(--deemphasized-text-color);
+ }
+ `,
+ ];
}
- _getPlugins(filter: string, pluginsPerPage: number, offset?: number) {
+ override render() {
+ return html`
+ <gr-list-view
+ .filter=${this.filter}
+ .itemsPerPage=${this.pluginsPerPage}
+ .items=${this.plugins}
+ .loading=${this.loading}
+ .offset=${this.offset}
+ .path=${this.path}
+ >
+ <table id="list" class="genericList">
+ <tbody>
+ <tr class="headerRow">
+ <th class="name topHeader">Plugin Name</th>
+ <th class="version topHeader">Version</th>
+ <th class="apiVersion topHeader">API Version</th>
+ <th class="status topHeader">Status</th>
+ </tr>
+ ${this.renderLoading()}
+ </tbody>
+ ${this.renderPluginListsTable()}
+ </table>
+ </gr-list-view>
+ `;
+ }
+
+ private renderLoading() {
+ if (!this.loading) return;
+
+ return html`
+ <tr id="loading" class="loadingMsg loading">
+ <td>Loading...</td>
+ </tr>
+ `;
+ }
+
+ private renderPluginListsTable() {
+ if (this.loading) return;
+
+ return html`
+ <tbody>
+ ${this.plugins
+ ?.slice(0, SHOWN_ITEMS_COUNT)
+ .map(plugin => this.renderPluginList(plugin))}
+ </tbody>
+ `;
+ }
+
+ private renderPluginList(plugin: PluginInfoWithName) {
+ return html`
+ <tr class="table">
+ <td class="name">
+ ${plugin.index_url
+ ? html`<a href=${this.computePluginUrl(plugin.index_url)}
+ >${plugin.id}</a
+ >`
+ : plugin.id}
+ </td>
+ <td class="version">
+ ${plugin.version
+ ? plugin.version
+ : html`<span class="placeholder">--</span>`}
+ </td>
+ <td class="apiVersion">
+ ${plugin.api_version
+ ? plugin.api_version
+ : html`<span class="placeholder">--</span>`}
+ </td>
+ <td class="status">
+ ${plugin.disabled === true ? 'Disabled' : 'Enabled'}
+ </td>
+ </tr>
+ `;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('params')) {
+ this.paramsChanged();
+ }
+ }
+
+ // private but used in test
+ paramsChanged() {
+ this.loading = true;
+ this.filter = this.params?.filter ?? '';
+ this.offset = Number(this.params?.offset ?? 0);
+
+ return this.getPlugins(this.filter, this.pluginsPerPage, this.offset);
+ }
+
+ private getPlugins(filter: string, pluginsPerPage: number, offset?: number) {
const errFn: ErrorCallback = response => {
firePageError(response);
};
@@ -95,31 +175,21 @@
.getPlugins(filter, pluginsPerPage, offset, errFn)
.then(plugins => {
if (!plugins) {
- this._plugins = [];
+ this.plugins = [];
return;
}
- this._plugins = Object.keys(plugins).map(key => {
+ this.plugins = Object.keys(plugins).map(key => {
return {...plugins[key], name: key};
});
- this._loading = false;
+ })
+ .finally(() => {
+ this.loading = false;
});
}
- _status(item: PluginInfo) {
- return item.disabled === true ? 'Disabled' : 'Enabled';
- }
-
- _computePluginUrl(id: string) {
+ private computePluginUrl(id: string) {
return getBaseUrl() + '/' + encodeURL(id, true);
}
-
- computeLoadingClass(loading: boolean) {
- return loading ? 'loading' : '';
- }
-
- computeShownItems(plugins: PluginInfoWithName[]) {
- return plugins.slice(0, SHOWN_ITEMS_COUNT);
- }
}
declare global {
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.ts b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.ts
deleted file mode 100644
index eeca478..0000000
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-table-styles">
- .placeholder {
- color: var(--deemphasized-text-color);
- }
- </style>
- <gr-list-view
- filter="[[_filter]]"
- items-per-page="[[_pluginsPerPage]]"
- items="[[_plugins]]"
- loading="[[_loading]]"
- offset="[[_offset]]"
- path="[[_path]]"
- >
- <table id="list" class="genericList">
- <tbody>
- <tr class="headerRow">
- <th class="name topHeader">Plugin Name</th>
- <th class="version topHeader">Version</th>
- <th class="apiVersion topHeader">API Version</th>
- <th class="status topHeader">Status</th>
- </tr>
- <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
- <td>Loading...</td>
- </tr>
- </tbody>
- <tbody class$="[[computeLoadingClass(_loading)]]">
- <template is="dom-repeat" items="[[_shownPlugins]]">
- <tr class="table">
- <td class="name">
- <template is="dom-if" if="[[item.index_url]]">
- <a href$="[[_computePluginUrl(item.index_url)]]">[[item.id]]</a>
- </template>
- <template is="dom-if" if="[[!item.index_url]]">
- [[item.id]]
- </template>
- </td>
- <td class="version">
- <template is="dom-if" if="[[item.version]]">
- [[item.version]]
- </template>
- <template is="dom-if" if="[[!item.version]]">
- <span class="placeholder">--</span>
- </template>
- </td>
- <td class="apiVersion">
- <template is="dom-if" if="[[item.api_version]]">
- [[item.api_version]]
- </template>
- <template is="dom-if" if="[[!item.api_version]]">
- <span class="placeholder">--</span>
- </template>
- </td>
- <td class="status">[[_status(item)]]</td>
- </tr>
- </template>
- </tbody>
- </table>
- </gr-list-view>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js
deleted file mode 100644
index 62c89b2..0000000
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js
+++ /dev/null
@@ -1,171 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-plugin-list.js';
-import 'lodash/lodash.js';
-import {
- addListenerForTest,
- mockPromise,
- stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-plugin-list');
-
-let counter;
-const pluginGenerator = () => {
- const plugin = {
- id: `test${++counter}`,
- disabled: false,
- };
-
- if (counter !== 2) {
- plugin.index_url = `plugins/test${counter}/`;
- }
- if (counter !== 3) {
- plugin.version = `version-${counter}`;
- }
- if (counter !== 4) {
- plugin.api_version = `api-version-${counter}`;
- }
- return plugin;
-};
-
-suite('gr-plugin-list tests', () => {
- let element;
- let plugins;
-
- let value;
-
- setup(() => {
- element = basicFixture.instantiate();
- counter = 0;
- });
-
- suite('list with plugins', async () => {
- setup(async () => {
- plugins = _.times(26, pluginGenerator);
- stubRestApi('getPlugins').returns(Promise.resolve(plugins));
- await element._paramsChanged(value);
- await flush();
- });
-
- test('plugin in the list is formatted correctly', async () => {
- await flush();
- assert.equal(element._plugins[4].id, 'test5');
- assert.equal(element._plugins[4].index_url, 'plugins/test5/');
- assert.equal(element._plugins[4].version, 'version-5');
- assert.equal(element._plugins[4].api_version, 'api-version-5');
- assert.equal(element._plugins[4].disabled, false);
- });
-
- test('with and without urls', async () => {
- await flush();
- const names = element.root.querySelectorAll('.name');
- assert.isOk(names[1].querySelector('a'));
- assert.equal(names[1].querySelector('a').innerText, 'test1');
- assert.isNotOk(names[2].querySelector('a'));
- assert.equal(names[2].innerText, 'test2');
- });
-
- test('versions', async () => {
- await flush();
- const versions = element.root.querySelectorAll('.version');
- assert.equal(versions[2].innerText, 'version-2');
- assert.equal(versions[3].innerText, '--');
- });
-
- test('api versions', async () => {
- await flush();
- const apiVersions = element.root.querySelectorAll(
- '.apiVersion');
- assert.equal(apiVersions[3].innerText, 'api-version-3');
- assert.equal(apiVersions[4].innerText, '--');
- });
-
- test('_shownPlugins', () => {
- assert.equal(element._shownPlugins.length, 25);
- });
- });
-
- suite('list with less then 26 plugins', () => {
- setup(async () => {
- plugins = _.times(25, pluginGenerator);
- stubRestApi('getPlugins').returns(Promise.resolve(plugins));
- await element._paramsChanged(value);
- await flush();
- });
-
- test('_shownPlugins', () => {
- assert.equal(element._shownPlugins.length, 25);
- });
- });
-
- suite('filter', () => {
- test('_paramsChanged', async () => {
- const getPluginsStub = stubRestApi('getPlugins');
- getPluginsStub.returns(Promise.resolve(plugins));
- const value = {
- filter: 'test',
- offset: 25,
- };
- await element._paramsChanged(value);
- assert.equal(getPluginsStub.lastCall.args[0], 'test');
- assert.equal(getPluginsStub.lastCall.args[1], 25);
- assert.equal(getPluginsStub.lastCall.args[2], 25);
- });
- });
-
- suite('loading', () => {
- test('correct contents are displayed', async () => {
- assert.isTrue(element._loading);
- assert.equal(element.computeLoadingClass(element._loading), 'loading');
- assert.equal(getComputedStyle(element.$.loading).display, 'block');
-
- element._loading = false;
- element._plugins = _.times(25, pluginGenerator);
-
- await flush();
- assert.equal(element.computeLoadingClass(element._loading), '');
- assert.equal(getComputedStyle(element.$.loading).display, 'none');
- });
- });
-
- suite('404', () => {
- test('fires page-error', async () => {
- const response = {status: 404};
- stubRestApi('getPlugins').callsFake(
- (filter, pluginsPerPage, opt_offset, errFn) => {
- errFn(response);
- return Promise.resolve(undefined);
- });
-
- const promise = mockPromise();
- addListenerForTest(document, 'page-error', e => {
- assert.deepEqual(e.detail.response, response);
- promise.resolve();
- });
-
- const value = {
- filter: 'test',
- offset: 25,
- };
- await element._paramsChanged(value);
- await promise;
- });
- });
-});
-
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.ts b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.ts
new file mode 100644
index 0000000..ca77a6d
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.ts
@@ -0,0 +1,200 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-plugin-list';
+import {GrPluginList, PluginInfoWithName} from './gr-plugin-list';
+import {
+ addListenerForTest,
+ mockPromise,
+ query,
+ queryAll,
+ queryAndAssert,
+ stubRestApi,
+} from '../../../test/test-utils';
+import {PluginInfo} from '../../../types/common';
+import {AppElementAdminParams} from '../../gr-app-types';
+import {GerritView} from '../../../services/router/router-model';
+import {PageErrorEvent} from '../../../types/events';
+import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
+
+const basicFixture = fixtureFromElement('gr-plugin-list');
+
+function pluginGenerator(counter: number) {
+ const plugin: PluginInfo = {
+ id: `test${counter}`,
+ version: `version-${counter}`,
+ disabled: false,
+ };
+
+ if (counter !== 2) {
+ plugin.index_url = `plugins/test${counter}/`;
+ }
+ if (counter !== 4) {
+ plugin.api_version = `api-version-${counter}`;
+ }
+ return plugin;
+}
+
+function createPluginList(n: number) {
+ const plugins = [];
+ for (let i = 0; i < n; ++i) {
+ const plugin = pluginGenerator(i) as PluginInfoWithName;
+ plugin.name = `test${i}`;
+ plugins.push(plugin);
+ }
+ return plugins;
+}
+
+function createPluginObjectList(n: number) {
+ const plugins: {[pluginName: string]: PluginInfo} | undefined = {};
+ for (let i = 0; i < n; ++i) {
+ plugins[`test${i}`] = pluginGenerator(i);
+ }
+ return plugins;
+}
+
+suite('gr-plugin-list tests', () => {
+ let element: GrPluginList;
+ let plugins: {[pluginName: string]: PluginInfo} | undefined;
+
+ const value: AppElementAdminParams = {view: GerritView.ADMIN, adminView: ''};
+
+ setup(async () => {
+ element = basicFixture.instantiate();
+ await element.updateComplete;
+ });
+
+ suite('list with plugins', async () => {
+ setup(async () => {
+ plugins = createPluginObjectList(26);
+ stubRestApi('getPlugins').returns(Promise.resolve(plugins));
+ element.params = value;
+ await element.paramsChanged();
+ await element.updateComplete;
+ });
+
+ test('plugin in the list is formatted correctly', async () => {
+ await element.updateComplete;
+ assert.equal(element.plugins![5].id, 'test5');
+ assert.equal(element.plugins![5].index_url, 'plugins/test5/');
+ assert.equal(element.plugins![5].version, 'version-5');
+ assert.equal(element.plugins![5].api_version, 'api-version-5');
+ assert.equal(element.plugins![5].disabled, false);
+ });
+
+ test('with and without urls', async () => {
+ await element.updateComplete;
+ const names = queryAll<HTMLTableElement>(element, '.name');
+ assert.isOk(queryAndAssert<HTMLAnchorElement>(names[2], 'a'));
+ assert.equal(
+ queryAndAssert<HTMLAnchorElement>(names[2], 'a').innerText,
+ 'test1'
+ );
+ assert.isNotOk(query(names[3], 'a'));
+ assert.equal(names[3].innerText, 'test2');
+ });
+
+ test('versions', async () => {
+ await element.updateComplete;
+ const versions = queryAll<HTMLTableElement>(element, '.version');
+ assert.equal(versions[3].innerText, 'version-2');
+ });
+
+ test('api versions', async () => {
+ await element.updateComplete;
+ const apiVersions = queryAll<HTMLTableElement>(element, '.apiVersion');
+ assert.equal(apiVersions[4].innerText, 'api-version-3');
+ assert.equal(apiVersions[5].innerText, '--');
+ });
+
+ test('plugins', () => {
+ assert.equal(element.plugins!.slice(0, SHOWN_ITEMS_COUNT).length, 25);
+ });
+ });
+
+ suite('list with less then 26 plugins', () => {
+ setup(async () => {
+ plugins = createPluginObjectList(25);
+ stubRestApi('getPlugins').returns(Promise.resolve(plugins));
+ element.params = value;
+ await element.paramsChanged();
+ await element.updateComplete;
+ });
+
+ test('plugins', () => {
+ assert.equal(element.plugins!.slice(0, SHOWN_ITEMS_COUNT).length, 25);
+ });
+ });
+
+ suite('filter', () => {
+ test('paramsChanged', async () => {
+ const getPluginsStub = stubRestApi('getPlugins');
+ getPluginsStub.returns(Promise.resolve(plugins));
+ value.filter = 'test';
+ value.offset = 25;
+ element.params = value;
+ await element.paramsChanged();
+ assert.equal(getPluginsStub.lastCall.args[0], 'test');
+ assert.equal(getPluginsStub.lastCall.args[1], 25);
+ assert.equal(getPluginsStub.lastCall.args[2], 25);
+ });
+ });
+
+ suite('loading', () => {
+ test('correct contents are displayed', async () => {
+ assert.isTrue(element.loading);
+ assert.equal(
+ getComputedStyle(queryAndAssert<HTMLTableElement>(element, '#loading'))
+ .display,
+ 'block'
+ );
+
+ element.loading = false;
+ element.plugins = createPluginList(25);
+
+ await element.updateComplete;
+ assert.isNotOk(query<HTMLTableElement>(element, '#loading'));
+ });
+ });
+
+ suite('404', () => {
+ test('fires page-error', async () => {
+ const response = {status: 404} as Response;
+ stubRestApi('getPlugins').callsFake(
+ (_filter, _pluginsPerPage, _opt_offset, errFn) => {
+ if (errFn !== undefined) {
+ errFn(response);
+ }
+ return Promise.resolve(undefined);
+ }
+ );
+
+ const promise = mockPromise();
+ addListenerForTest(document, 'page-error', e => {
+ assert.deepEqual((e as PageErrorEvent).detail.response, response);
+ promise.resolve();
+ });
+
+ value.filter = 'test';
+ value.offset = 25;
+ element.params = value;
+ await element.paramsChanged();
+ await promise;
+ });
+ });
+});
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 56e981a..cf5d952 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
@@ -49,7 +49,7 @@
PrimitiveValue,
} from './gr-repo-access-interfaces';
import {firePageError, fireAlert} from '../../../utils/event-util';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {WebLinkInfo} from '../../../types/diff';
const NOTHING_TO_SAVE = 'No changes to save.';
@@ -115,9 +115,10 @@
@property({type: Boolean})
_loading = true;
- private originalInheritsFrom?: ProjectInfo;
+ // private but used in the tests
+ originalInheritsFrom?: ProjectInfo;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
@@ -131,7 +132,7 @@
this._modified = true;
}
- _repoChanged(repo: RepoName) {
+ _repoChanged(repo?: RepoName) {
this._loading = true;
if (!repo) {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts
similarity index 65%
rename from polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js
rename to polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts
index 1ccfd5e..a5159ae 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts
@@ -15,23 +15,42 @@
* limitations under the License.
*/
-import '../../../test/common-test-setup-karma.js';
-import './gr-repo-access.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {toSortedPermissionsArray} from '../../../utils/access-util.js';
+import '../../../test/common-test-setup-karma';
+import './gr-repo-access';
+import {GrRepoAccess} from './gr-repo-access';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {toSortedPermissionsArray} from '../../../utils/access-util';
import {
addListenerForTest,
mockPromise,
+ queryAll,
+ queryAndAssert,
stubRestApi,
-} from '../../../test/test-utils.js';
+} from '../../../test/test-utils';
+import {
+ ChangeInfo,
+ GitRef,
+ RepoName,
+ UrlEncodedRepoName,
+} from '../../../types/common';
+import {PermissionAction} from '../../../constants/constants';
+import {PageErrorEvent} from '../../../types/events';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {
+ AutocompleteCommitEvent,
+ GrAutocomplete,
+} from '../../shared/gr-autocomplete/gr-autocomplete';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+import {GrAccessSection} from '../gr-access-section/gr-access-section';
+import {GrPermission} from '../gr-permission/gr-permission';
+import {createChange} from '../../../test/test-data-generators';
const basicFixture = fixtureFromElement('gr-repo-access');
suite('gr-repo-access tests', () => {
- let element;
+ let element: GrRepoAccess;
- let repoStub;
+ let repoStub: sinon.SinonStub;
const accessRes = {
local: {
@@ -39,13 +58,13 @@
permissions: {
owner: {
rules: {
- 234: {action: 'ALLOW'},
- 123: {action: 'DENY'},
+ 234: {action: PermissionAction.ALLOW},
+ 123: {action: PermissionAction.DENY},
},
},
read: {
rules: {
- 234: {action: 'ALLOW'},
+ 234: {action: PermissionAction.ALLOW},
},
},
},
@@ -59,11 +78,13 @@
name: 'Maintainers',
},
},
- config_web_links: [{
- name: 'gitiles',
- target: '_blank',
- url: 'https://my/site/+log/123/project.config',
- }],
+ config_web_links: [
+ {
+ name: 'gitiles',
+ target: '_blank',
+ url: 'https://my/site/+log/123/project.config',
+ },
+ ],
can_upload: true,
};
const accessRes2 = {
@@ -73,7 +94,7 @@
accessDatabase: {
rules: {
group1: {
- action: 'ALLOW',
+ action: PermissionAction.ALLOW,
},
},
},
@@ -82,15 +103,17 @@
},
};
const repoRes = {
+ id: '' as UrlEncodedRepoName,
labels: {
'Code-Review': {
values: {
- ' 0': 'No score',
+ '0': 'No score',
'-1': 'I would prefer this is not merged as is',
'-2': 'This shall not be merged',
'+1': 'Looks good to me, but someone else must approve',
'+2': 'Looks good to me, approved',
},
+ default_value: 0,
},
},
};
@@ -106,7 +129,7 @@
};
setup(async () => {
element = basicFixture.instantiate();
- stubRestApi('getAccount').returns(Promise.resolve(null));
+ stubRestApi('getAccount').returns(Promise.resolve(undefined));
repoStub = stubRestApi('getRepo').returns(Promise.resolve(repoRes));
element._loading = false;
element._ownerOf = [];
@@ -115,43 +138,51 @@
});
test('_repoChanged called when repo name changes', async () => {
- sinon.stub(element, '_repoChanged');
- element.repo = 'New Repo';
+ const repoChangedStub = sinon.stub(element, '_repoChanged');
+ element.repo = 'New Repo' as RepoName;
await flush();
- assert.isTrue(element._repoChanged.called);
+ assert.isTrue(repoChangedStub.called);
});
test('_repoChanged', async () => {
- const accessStub = stubRestApi(
- 'getRepoAccessRights');
+ const accessStub = stubRestApi('getRepoAccessRights');
- accessStub.withArgs('New Repo').returns(
- Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
- accessStub.withArgs('Another New Repo')
- .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2))));
- const capabilitiesStub = stubRestApi(
- 'getCapabilities');
+ accessStub
+ .withArgs('New Repo' as RepoName)
+ .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
+ accessStub
+ .withArgs('Another New Repo' as RepoName)
+ .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2))));
+ const capabilitiesStub = stubRestApi('getCapabilities');
capabilitiesStub.returns(Promise.resolve(capabilitiesRes));
- await element._repoChanged('New Repo');
+ await element._repoChanged('New Repo' as RepoName);
assert.isTrue(accessStub.called);
assert.isTrue(capabilitiesStub.called);
assert.isTrue(repoStub.called);
assert.isNotOk(element._inheritsFrom);
assert.deepEqual(element._local, accessRes.local);
- assert.deepEqual(element._sections,
- toSortedPermissionsArray(accessRes.local));
+ assert.deepEqual(
+ element._sections,
+ toSortedPermissionsArray(accessRes.local)
+ );
assert.deepEqual(element._labels, repoRes.labels);
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('.weblinks')).display,
- 'block');
+ assert.equal(
+ getComputedStyle(queryAndAssert<HTMLDivElement>(element, '.weblinks'))
+ .display,
+ 'block'
+ );
- await element._repoChanged('Another New Repo');
- assert.deepEqual(element._sections,
- toSortedPermissionsArray(accessRes2.local));
- assert.equal(getComputedStyle(element.shadowRoot
- .querySelector('.weblinks')).display,
- 'none');
+ await element._repoChanged('Another New Repo' as RepoName);
+ assert.deepEqual(
+ element._sections,
+ toSortedPermissionsArray(accessRes2.local)
+ );
+ assert.equal(
+ getComputedStyle(queryAndAssert<HTMLDivElement>(element, '.weblinks'))
+ .display,
+ 'none'
+ );
});
test('_repoChanged when repo changes to undefined returns', async () => {
@@ -161,10 +192,12 @@
name: 'Access Database',
},
};
- const accessStub = stubRestApi('getRepoAccessRights')
- .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2))));
- const capabilitiesStub = stubRestApi(
- 'getCapabilities').returns(Promise.resolve(capabilitiesRes));
+ const accessStub = stubRestApi('getRepoAccessRights').returns(
+ Promise.resolve(JSON.parse(JSON.stringify(accessRes2)))
+ );
+ const capabilitiesStub = stubRestApi('getCapabilities').returns(
+ Promise.resolve(capabilitiesRes)
+ );
await element._repoChanged();
assert.isFalse(accessStub.called);
@@ -173,34 +206,39 @@
});
test('_computeParentHref', () => {
- const repoName = 'test-repo';
- assert.equal(element._computeParentHref(repoName),
- '/admin/repos/test-repo,access');
+ assert.equal(
+ element._computeParentHref('test-repo' as RepoName),
+ '/admin/repos/test-repo,access'
+ );
});
test('_computeMainClass', () => {
- let ownerOf = ['refs/*'];
+ let ownerOf = ['refs/*'] as GitRef[];
const editing = true;
const canUpload = false;
- assert.equal(element._computeMainClass(ownerOf, canUpload), 'admin');
- assert.equal(element._computeMainClass(ownerOf, canUpload, editing),
- 'admin editing');
+ assert.equal(element._computeMainClass(ownerOf, canUpload, false), 'admin');
+ assert.equal(
+ element._computeMainClass(ownerOf, canUpload, editing),
+ 'admin editing'
+ );
ownerOf = [];
- assert.equal(element._computeMainClass(ownerOf, canUpload), '');
- assert.equal(element._computeMainClass(ownerOf, canUpload, editing),
- 'editing');
+ assert.equal(element._computeMainClass(ownerOf, canUpload, false), '');
+ assert.equal(
+ element._computeMainClass(ownerOf, canUpload, editing),
+ 'editing'
+ );
});
test('inherit section', async () => {
element._local = {};
element._ownerOf = [];
- sinon.stub(element, '_computeParentHref');
+ const computeParentHrefStub = sinon.stub(element, '_computeParentHref');
await flush();
// Nothing should appear when no inherit from and not in edit mode.
assert.equal(getComputedStyle(element.$.inheritsFrom).display, 'none');
// The autocomplete should be hidden, and the link should be displayed.
- assert.isFalse(element._computeParentHref.called);
+ assert.isFalse(computeParentHrefStub.called);
// When in edit mode, the autocomplete should appear.
element._editing = true;
// When editing, the autocomplete should still not be shown.
@@ -208,33 +246,45 @@
element._editing = false;
element._inheritsFrom = {
- id: '1234',
- name: 'another-repo',
+ id: '1234' as UrlEncodedRepoName,
+ name: 'another-repo' as RepoName,
};
await flush();
// When there is a parent project, the link should be displayed.
assert.notEqual(getComputedStyle(element.$.inheritsFrom).display, 'none');
- assert.notEqual(getComputedStyle(element.$.inheritFromName).display,
- 'none');
- assert.equal(getComputedStyle(element.$.editInheritFromInput).display,
- 'none');
- assert.isTrue(element._computeParentHref.called);
+ assert.notEqual(
+ getComputedStyle(element.$.inheritFromName).display,
+ 'none'
+ );
+ assert.equal(
+ getComputedStyle(
+ queryAndAssert<GrAutocomplete>(element, '#editInheritFromInput')
+ ).display,
+ 'none'
+ );
+ assert.isTrue(computeParentHrefStub.called);
element._editing = true;
// When editing, the autocomplete should be shown.
assert.notEqual(getComputedStyle(element.$.inheritsFrom).display, 'none');
assert.equal(getComputedStyle(element.$.inheritFromName).display, 'none');
- assert.notEqual(getComputedStyle(element.$.editInheritFromInput).display,
- 'none');
+ assert.notEqual(
+ getComputedStyle(
+ queryAndAssert<GrAutocomplete>(element, '#editInheritFromInput')
+ ).display,
+ 'none'
+ );
});
test('_handleUpdateInheritFrom', async () => {
- element._inheritFromFilter = 'foo bar baz';
- element._handleUpdateInheritFrom({detail: {value: 'abc+123'}});
+ element._inheritFromFilter = 'foo bar baz' as RepoName;
+ element._handleUpdateInheritFrom({
+ detail: {value: 'abc+123'},
+ } as CustomEvent);
await flush();
assert.isOk(element._inheritsFrom);
- assert.equal(element._inheritsFrom.id, 'abc+123');
- assert.equal(element._inheritsFrom.name, 'foo bar baz');
+ assert.equal(element._inheritsFrom!.id, 'abc+123');
+ assert.equal(element._inheritsFrom!.name, 'foo bar baz' as RepoName);
});
test('_computeLoadingClass', () => {
@@ -243,84 +293,113 @@
});
test('fires page-error', async () => {
- const response = {status: 404};
+ const response = {status: 404} as Response;
- stubRestApi('getRepoAccessRights').callsFake((repoName, errFn) => {
- errFn(response);
+ stubRestApi('getRepoAccessRights').callsFake((_repoName, errFn) => {
+ if (errFn !== undefined) {
+ errFn(response);
+ }
return Promise.resolve(undefined);
});
const promise = mockPromise();
addListenerForTest(document, 'page-error', e => {
- assert.deepEqual(e.detail.response, response);
+ assert.deepEqual((e as PageErrorEvent).detail.response, response);
promise.resolve();
});
- element.repo = 'test';
+ element.repo = 'test' as RepoName;
await promise;
});
suite('with defined sections', () => {
const testEditSaveCancelBtns = async (
- shouldShowSave,
- shouldShowSaveReview
+ shouldShowSave: boolean,
+ shouldShowSaveReview: boolean
) => {
// Edit button is visible and Save button is hidden.
- assert.equal(getComputedStyle(element.$.saveReviewBtn).display, 'none');
- assert.equal(getComputedStyle(element.$.saveBtn).display, 'none');
- assert.notEqual(getComputedStyle(element.$.editBtn).display, 'none');
- assert.equal(element.$.editBtn.innerText, 'EDIT');
assert.equal(
- getComputedStyle(element.$.editInheritFromInput).display,
- 'none'
+ getComputedStyle(queryAndAssert<GrButton>(element, '#saveReviewBtn'))
+ .display,
+ 'none'
+ );
+ assert.equal(
+ getComputedStyle(queryAndAssert<GrButton>(element, '#saveBtn')).display,
+ 'none'
+ );
+ assert.notEqual(
+ getComputedStyle(queryAndAssert<GrButton>(element, '#editBtn')).display,
+ 'none'
+ );
+ assert.equal(
+ queryAndAssert<GrButton>(element, '#editBtn').innerText,
+ 'EDIT'
+ );
+ assert.equal(
+ getComputedStyle(
+ queryAndAssert<GrAutocomplete>(element, '#editInheritFromInput')
+ ).display,
+ 'none'
);
element._inheritsFrom = {
- id: 'test-project',
+ id: 'test-project' as UrlEncodedRepoName,
};
await flush();
assert.equal(
- getComputedStyle(
- element.shadowRoot.querySelector('#editInheritFromInput')
- ).display,
- 'none'
+ getComputedStyle(
+ queryAndAssert<GrAutocomplete>(element, '#editInheritFromInput')
+ ).display,
+ 'none'
);
- MockInteractions.tap(element.$.editBtn);
+ MockInteractions.tap(queryAndAssert<GrButton>(element, '#editBtn'));
await flush();
// Edit button changes to Cancel button, and Save button is visible but
// disabled.
- assert.equal(element.$.editBtn.innerText, 'CANCEL');
+ assert.equal(
+ queryAndAssert<GrButton>(element, '#editBtn').innerText,
+ 'CANCEL'
+ );
if (shouldShowSaveReview) {
assert.notEqual(
- getComputedStyle(element.$.saveReviewBtn).display,
- 'none'
+ getComputedStyle(queryAndAssert<GrButton>(element, '#saveReviewBtn'))
+ .display,
+ 'none'
);
- assert.isTrue(element.$.saveReviewBtn.disabled);
+ assert.isTrue(
+ queryAndAssert<GrButton>(element, '#saveReviewBtn').disabled
+ );
}
if (shouldShowSave) {
- assert.notEqual(getComputedStyle(element.$.saveBtn).display, 'none');
- assert.isTrue(element.$.saveBtn.disabled);
+ assert.notEqual(
+ getComputedStyle(queryAndAssert<GrButton>(element, '#saveBtn'))
+ .display,
+ 'none'
+ );
+ assert.isTrue(queryAndAssert<GrButton>(element, '#saveBtn').disabled);
}
assert.notEqual(
- getComputedStyle(
- element.shadowRoot.querySelector('#editInheritFromInput')
- ).display,
- 'none'
+ getComputedStyle(
+ queryAndAssert<GrAutocomplete>(element, '#editInheritFromInput')
+ ).display,
+ 'none'
);
// Save button should be enabled after access is modified
element.dispatchEvent(
- new CustomEvent('access-modified', {
- composed: true,
- bubbles: true,
- })
+ new CustomEvent('access-modified', {
+ composed: true,
+ bubbles: true,
+ })
);
if (shouldShowSaveReview) {
- assert.isFalse(element.$.saveReviewBtn.disabled);
+ assert.isFalse(
+ queryAndAssert<GrButton>(element, '#saveReviewBtn').disabled
+ );
}
if (shouldShowSave) {
- assert.isFalse(element.$.saveBtn.disabled);
+ assert.isFalse(queryAndAssert<GrButton>(element, '#saveBtn').disabled);
}
};
@@ -337,16 +416,20 @@
});
test('removing an added section', async () => {
- element.editing = true;
+ element._editing = true;
await flush();
- assert.equal(element._sections.length, 1);
- element.shadowRoot
- .querySelector('gr-access-section').dispatchEvent(
- new CustomEvent('added-section-removed', {
- composed: true, bubbles: true,
- }));
+ assert.equal(element._sections!.length, 1);
+ queryAndAssert<GrAccessSection>(
+ element,
+ 'gr-access-section'
+ ).dispatchEvent(
+ new CustomEvent('added-section-removed', {
+ composed: true,
+ bubbles: true,
+ })
+ );
await flush();
- assert.equal(element._sections.length, 0);
+ assert.equal(element._sections!.length, 0);
});
test('button visibility for non ref owner', () => {
@@ -354,64 +437,77 @@
assert.equal(getComputedStyle(element.$.editBtn).display, 'none');
});
- test('button visibility for non ref owner with upload privilege',
- async () => {
- element._canUpload = true;
- await flush();
- testEditSaveCancelBtns(false, true);
- });
+ test('button visibility for non ref owner with upload privilege', async () => {
+ element._canUpload = true;
+ await flush();
+ testEditSaveCancelBtns(false, true);
+ });
test('button visibility for ref owner', async () => {
- element._ownerOf = ['refs/for/*'];
+ element._ownerOf = ['refs/for/*'] as GitRef[];
await flush();
testEditSaveCancelBtns(true, false);
});
test('button visibility for ref owner and upload', async () => {
- element._ownerOf = ['refs/for/*'];
+ element._ownerOf = ['refs/for/*'] as GitRef[];
element._canUpload = true;
await flush();
testEditSaveCancelBtns(true, false);
});
test('_handleAccessModified called with event fired', async () => {
- sinon.spy(element, '_handleAccessModified');
+ const handleAccessModifiedSpy = sinon.spy(
+ element,
+ '_handleAccessModified'
+ );
element.dispatchEvent(
- new CustomEvent('access-modified', {
- composed: true, bubbles: true,
- }));
+ new CustomEvent('access-modified', {
+ composed: true,
+ bubbles: true,
+ })
+ );
await flush();
- assert.isTrue(element._handleAccessModified.called);
+ assert.isTrue(handleAccessModifiedSpy.called);
});
test('_handleAccessModified called when parent changes', async () => {
element._inheritsFrom = {
- id: 'test-project',
+ id: 'test-project' as UrlEncodedRepoName,
};
await flush();
- element.shadowRoot.querySelector('#editInheritFromInput').dispatchEvent(
- new CustomEvent('commit', {
- detail: {},
- composed: true, bubbles: true,
- }));
- sinon.spy(element, '_handleAccessModified');
+ queryAndAssert<GrAutocomplete>(
+ element,
+ '#editInheritFromInput'
+ ).dispatchEvent(
+ new CustomEvent('commit', {
+ detail: {},
+ composed: true,
+ bubbles: true,
+ })
+ );
+ const handleAccessModifiedSpy = sinon.spy(
+ element,
+ '_handleAccessModified'
+ );
element.dispatchEvent(
- new CustomEvent('access-modified', {
- detail: {},
- composed: true, bubbles: true,
- }));
+ new CustomEvent('access-modified', {
+ detail: {},
+ composed: true,
+ bubbles: true,
+ })
+ );
await flush();
- assert.isTrue(element._handleAccessModified.called);
+ assert.isTrue(handleAccessModifiedSpy.called);
});
test('_handleSaveForReview', async () => {
- const saveStub =
- stubRestApi('setRepoAccessRightsForReview');
+ const saveStub = stubRestApi('setRepoAccessRightsForReview');
sinon.stub(element, '_computeAddAndRemove').returns({
add: {},
remove: {},
});
- element._handleSaveForReview();
+ element._handleSaveForReview(new Event('test'));
await flush();
assert.isFalse(saveStub.called);
});
@@ -522,29 +618,35 @@
test('_handleSaveForReview parent change', async () => {
element._inheritsFrom = {
- id: 'test-project',
+ id: 'test-project' as UrlEncodedRepoName,
};
- element._originalInheritsFrom = {
- id: 'test-project-original',
+ element.originalInheritsFrom = {
+ id: 'test-project-original' as UrlEncodedRepoName,
};
await flush();
assert.deepEqual(element._computeAddAndRemove(), {
- parent: 'test-project', add: {}, remove: {},
+ parent: 'test-project',
+ add: {},
+ remove: {},
});
});
test('_handleSaveForReview new parent with spaces', async () => {
- element._inheritsFrom = {id: 'spaces+in+project+name'};
- element._originalInheritsFrom = {id: 'old-project'};
+ element._inheritsFrom = {
+ id: 'spaces+in+project+name' as UrlEncodedRepoName,
+ };
+ element.originalInheritsFrom = {id: 'old-project' as UrlEncodedRepoName};
await flush();
assert.deepEqual(element._computeAddAndRemove(), {
- parent: 'spaces in project name', add: {}, remove: {},
+ parent: 'spaces in project name',
+ add: {},
+ remove: {},
});
});
test('_handleSaveForReview rules', async () => {
// Delete a rule.
- element._local['refs/*'].permissions.owner.rules[123].deleted = true;
+ element._local!['refs/*'].permissions.owner.rules[123].deleted = true;
await flush();
let expectedInput = {
add: {},
@@ -563,10 +665,10 @@
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
// Undo deleting a rule.
- delete element._local['refs/*'].permissions.owner.rules[123].deleted;
+ delete element._local!['refs/*'].permissions.owner.rules[123].deleted;
// Modify a rule.
- element._local['refs/*'].permissions.owner.rules[123].modified = true;
+ element._local!['refs/*'].permissions.owner.rules[123].modified = true;
await flush();
expectedInput = {
add: {
@@ -597,7 +699,9 @@
test('_computeAddAndRemove permissions', async () => {
// Add a new rule to a permission.
- let expectedInput = {
+ let expectedInput = {};
+
+ expectedInput = {
add: {
'refs/*': {
permissions: {
@@ -614,22 +718,27 @@
},
remove: {},
};
-
- element.shadowRoot
- .querySelector('gr-access-section').shadowRoot
- .querySelector('gr-permission')
- ._handleAddRuleItem(
- {detail: {value: 'Maintainers'}});
+ const grAccessSection = queryAndAssert<GrAccessSection>(
+ element,
+ 'gr-access-section'
+ );
+ queryAndAssert<GrPermission>(
+ grAccessSection,
+ 'gr-permission'
+ )._handleAddRuleItem({
+ detail: {value: 'Maintainers'},
+ } as AutocompleteCommitEvent);
await flush();
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
// Remove the added rule.
- delete element._local['refs/*'].permissions.owner.rules.Maintainers;
+ delete element._local!['refs/*'].permissions.owner.rules.Maintainers;
// Delete a permission.
- element._local['refs/*'].permissions.owner.deleted = true;
+ element._local!['refs/*'].permissions.owner.deleted = true;
await flush();
+
expectedInput = {
add: {},
remove: {
@@ -643,10 +752,10 @@
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
// Undo delete permission.
- delete element._local['refs/*'].permissions.owner.deleted;
+ delete element._local!['refs/*'].permissions.owner.deleted;
// Modify a permission.
- element._local['refs/*'].permissions.owner.modified = true;
+ element._local!['refs/*'].permissions.owner.modified = true;
await flush();
expectedInput = {
add: {
@@ -675,7 +784,9 @@
test('_computeAddAndRemove sections', async () => {
// Add a new permission to a section
- let expectedInput = {
+ let expectedInput = {};
+
+ expectedInput = {
add: {
'refs/*': {
permissions: {
@@ -689,8 +800,10 @@
},
remove: {},
};
- element.shadowRoot
- .querySelector('gr-access-section')._handleAddPermission();
+ queryAndAssert<GrAccessSection>(
+ element,
+ 'gr-access-section'
+ )._handleAddPermission();
await flush();
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
@@ -716,18 +829,23 @@
},
remove: {},
};
- const newPermission =
- dom(element.shadowRoot
- .querySelector('gr-access-section').root).querySelectorAll(
- 'gr-permission')[2];
- newPermission._handleAddRuleItem(
- {detail: {value: 'Maintainers'}});
+ const grAccessSection = queryAndAssert<GrAccessSection>(
+ element,
+ 'gr-access-section'
+ );
+ const newPermission = queryAll<GrPermission>(
+ grAccessSection,
+ 'gr-permission'
+ )[2];
+ newPermission._handleAddRuleItem({
+ detail: {value: 'Maintainers'},
+ } as AutocompleteCommitEvent);
await flush();
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
// Modify a section reference.
- element._local['refs/*'].updatedId = 'refs/for/bar';
- element._local['refs/*'].modified = true;
+ element._local!['refs/*'].updatedId = 'refs/for/bar';
+ element._local!['refs/*'].modified = true;
await flush();
expectedInput = {
add: {
@@ -735,13 +853,13 @@
modified: true,
updatedId: 'refs/for/bar',
permissions: {
- 'owner': {
+ owner: {
rules: {
234: {action: 'ALLOW'},
123: {action: 'DENY'},
},
},
- 'read': {
+ read: {
rules: {
234: {action: 'ALLOW'},
},
@@ -771,7 +889,7 @@
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
// Delete a section.
- element._local['refs/*'].deleted = true;
+ element._local!['refs/*'].deleted = true;
await flush();
expectedInput = {
add: {},
@@ -786,7 +904,9 @@
test('_computeAddAndRemove new section', async () => {
// Add a new permission to a section
- let expectedInput = {
+ let expectedInput = {};
+
+ expectedInput = {
add: {
'refs/for/*': {
added: true,
@@ -814,8 +934,10 @@
},
remove: {},
};
- const newSection = dom(element.root)
- .querySelectorAll('gr-access-section')[1];
+ const newSection = queryAll<GrAccessSection>(
+ element,
+ 'gr-access-section'
+ )[1];
newSection._handleAddPermission();
await flush();
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
@@ -844,14 +966,17 @@
remove: {},
};
- newSection.shadowRoot
- .querySelector('gr-permission')._handleAddRuleItem(
- {detail: {value: 'Maintainers'}});
+ queryAndAssert<GrPermission>(
+ newSection,
+ 'gr-permission'
+ )._handleAddRuleItem({
+ detail: {value: 'Maintainers'},
+ } as AutocompleteCommitEvent);
await flush();
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
// Modify a the reference from the default value.
- element._local['refs/for/*'].updatedId = 'refs/for/new';
+ element._local!['refs/for/*'].updatedId = 'refs/for/new';
await flush();
expectedInput = {
add: {
@@ -881,10 +1006,12 @@
test('_computeAddAndRemove combinations', async () => {
// Modify rule and delete permission that it is inside of.
- element._local['refs/*'].permissions.owner.rules[123].modified = true;
- element._local['refs/*'].permissions.owner.deleted = true;
+ element._local!['refs/*'].permissions.owner.rules[123].modified = true;
+ element._local!['refs/*'].permissions.owner.deleted = true;
await flush();
- let expectedInput = {
+ let expectedInput = {};
+
+ expectedInput = {
add: {},
remove: {
'refs/*': {
@@ -896,13 +1023,13 @@
};
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
// Delete rule and delete permission that it is inside of.
- element._local['refs/*'].permissions.owner.rules[123].modified = false;
- element._local['refs/*'].permissions.owner.rules[123].deleted = true;
+ element._local!['refs/*'].permissions.owner.rules[123].modified = false;
+ element._local!['refs/*'].permissions.owner.rules[123].deleted = true;
await flush();
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
// Also modify a different rule inside of another permission.
- element._local['refs/*'].permissions.read.modified = true;
+ element._local!['refs/*'].permissions.read.modified = true;
await flush();
expectedInput = {
add: {
@@ -929,10 +1056,10 @@
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
// Modify both permissions with an exclusive bit. Owner is still
// deleted.
- element._local['refs/*'].permissions.owner.exclusive = true;
- element._local['refs/*'].permissions.owner.modified = true;
- element._local['refs/*'].permissions.read.exclusive = true;
- element._local['refs/*'].permissions.read.modified = true;
+ element._local!['refs/*'].permissions.owner.exclusive = true;
+ element._local!['refs/*'].permissions.owner.modified = true;
+ element._local!['refs/*'].permissions.read.exclusive = true;
+ element._local!['refs/*'].permissions.read.modified = true;
await flush();
expectedInput = {
add: {
@@ -960,12 +1087,17 @@
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
// Add a rule to the existing permission;
- const readPermission =
- dom(element.shadowRoot
- .querySelector('gr-access-section').root).querySelectorAll(
- 'gr-permission')[1];
- readPermission._handleAddRuleItem(
- {detail: {value: 'Maintainers'}});
+ const grAccessSection = queryAndAssert<GrAccessSection>(
+ element,
+ 'gr-access-section'
+ );
+ const readPermission = queryAll<GrPermission>(
+ grAccessSection,
+ 'gr-permission'
+ )[1];
+ readPermission._handleAddRuleItem({
+ detail: {value: 'Maintainers'},
+ } as AutocompleteCommitEvent);
await flush();
expectedInput = {
@@ -995,8 +1127,8 @@
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
// Change one of the refs
- element._local['refs/*'].updatedId = 'refs/for/bar';
- element._local['refs/*'].modified = true;
+ element._local!['refs/*'].updatedId = 'refs/for/bar';
+ element._local!['refs/*'].modified = true;
await flush();
expectedInput = {
@@ -1032,21 +1164,26 @@
},
},
};
- element._local['refs/*'].deleted = true;
+ element._local!['refs/*'].deleted = true;
await flush();
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
// Add a new section.
MockInteractions.tap(element.$.addReferenceBtn);
- let newSection = dom(element.root)
- .querySelectorAll('gr-access-section')[1];
+ let newSection = queryAll<GrAccessSection>(
+ element,
+ 'gr-access-section'
+ )[1];
newSection._handleAddPermission();
await flush();
- newSection.shadowRoot
- .querySelector('gr-permission')._handleAddRuleItem(
- {detail: {value: 'Maintainers'}});
+ queryAndAssert<GrPermission>(
+ newSection,
+ 'gr-permission'
+ )._handleAddRuleItem({
+ detail: {value: 'Maintainers'},
+ } as AutocompleteCommitEvent);
// Modify a the reference from the default value.
- element._local['refs/for/*'].updatedId = 'refs/for/new';
+ element._local!['refs/for/*'].updatedId = 'refs/for/new';
await flush();
expectedInput = {
@@ -1079,8 +1216,9 @@
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
// Modify newly added rule inside new ref.
- element._local['refs/for/*'].permissions['label-Code-Review'].
- rules['Maintainers'].modified = true;
+ element._local!['refs/for/*'].permissions['label-Code-Review'].rules[
+ 'Maintainers'
+ ].modified = true;
await flush();
expectedInput = {
add: {
@@ -1115,15 +1253,17 @@
// Add a second new section.
MockInteractions.tap(element.$.addReferenceBtn);
await flush();
- newSection = dom(element.root)
- .querySelectorAll('gr-access-section')[2];
+ newSection = queryAll<GrAccessSection>(element, 'gr-access-section')[2];
newSection._handleAddPermission();
await flush();
- newSection.shadowRoot
- .querySelector('gr-permission')._handleAddRuleItem(
- {detail: {value: 'Maintainers'}});
+ queryAndAssert<GrPermission>(
+ newSection,
+ 'gr-permission'
+ )._handleAddRuleItem({
+ detail: {value: 'Maintainers'},
+ } as AutocompleteCommitEvent);
// Modify a the reference from the default value.
- element._local['refs/for/**'].updatedId = 'refs/for/new2';
+ element._local!['refs/for/**'].updatedId = 'refs/for/new2';
await flush();
expectedInput = {
add: {
@@ -1178,16 +1318,16 @@
// Unsaved changes are discarded when editing is cancelled.
MockInteractions.tap(element.$.editBtn);
await flush();
- assert.equal(element._sections.length, 1);
- assert.equal(Object.keys(element._local).length, 1);
+ assert.equal(element._sections!.length, 1);
+ assert.equal(Object.keys(element._local!).length, 1);
MockInteractions.tap(element.$.addReferenceBtn);
await flush();
- assert.equal(element._sections.length, 2);
- assert.equal(Object.keys(element._local).length, 2);
+ assert.equal(element._sections!.length, 2);
+ assert.equal(Object.keys(element._local!).length, 2);
MockInteractions.tap(element.$.editBtn);
await flush();
- assert.equal(element._sections.length, 1);
- assert.equal(Object.keys(element._local).length, 1);
+ assert.equal(element._sections!.length, 1);
+ assert.equal(Object.keys(element._local!).length, 1);
});
test('_handleSave', async () => {
@@ -1216,24 +1356,25 @@
},
};
stubRestApi('getRepoAccessRights').returns(
- Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
- sinon.stub(GerritNav, 'navigateToChange');
- let resolver;
- const saveStub = stubRestApi(
- 'setRepoAccessRights')
- .returns(new Promise(r => resolver = r));
+ Promise.resolve(JSON.parse(JSON.stringify(accessRes)))
+ );
+ const navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
+ let resolver: (value: Response | PromiseLike<Response>) => void;
+ const saveStub = stubRestApi('setRepoAccessRights').returns(
+ new Promise(r => (resolver = r))
+ );
- element.repo = 'test-repo';
+ element.repo = 'test-repo' as RepoName;
sinon.stub(element, '_computeAddAndRemove').returns(repoAccessInput);
element._modified = true;
MockInteractions.tap(element.$.saveBtn);
await flush();
assert.equal(element.$.saveBtn.hasAttribute('loading'), true);
- resolver({_number: 1});
+ resolver!({status: 200} as Response);
await flush();
assert.isTrue(saveStub.called);
- assert.isTrue(GerritNav.navigateToChange.notCalled);
+ assert.isTrue(navigateToChangeStub.notCalled);
});
test('_handleSaveForReview', async () => {
@@ -1262,26 +1403,27 @@
},
};
stubRestApi('getRepoAccessRights').returns(
- Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
- sinon.stub(GerritNav, 'navigateToChange');
- let resolver;
+ Promise.resolve(JSON.parse(JSON.stringify(accessRes)))
+ );
+ const navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
+ let resolver: (value: ChangeInfo | PromiseLike<ChangeInfo>) => void;
const saveForReviewStub = stubRestApi(
- 'setRepoAccessRightsForReview')
- .returns(new Promise(r => resolver = r));
+ 'setRepoAccessRightsForReview'
+ ).returns(new Promise(r => (resolver = r)));
- element.repo = 'test-repo';
+ element.repo = 'test-repo' as RepoName;
sinon.stub(element, '_computeAddAndRemove').returns(repoAccessInput);
element._modified = true;
MockInteractions.tap(element.$.saveReviewBtn);
await flush();
assert.equal(element.$.saveReviewBtn.hasAttribute('loading'), true);
- resolver({_number: 1});
+ resolver!(createChange());
await flush();
assert.isTrue(saveForReviewStub.called);
- assert.isTrue(GerritNav.navigateToChange
- .lastCall.calledWithExactly({_number: 1}));
+ assert.isTrue(
+ navigateToChangeStub.lastCall.calledWithExactly(createChange())
+ );
});
});
});
-
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 de43dc6..990b34d 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
@@ -14,20 +14,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
-import '../../../styles/gr-font-styles';
-import '../../../styles/gr-form-styles';
-import '../../../styles/gr-subpage-styles';
-import '../../../styles/shared-styles';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
+import '../../shared/gr-button/gr-button';
import '../../shared/gr-dialog/gr-dialog';
import '../../shared/gr-overlay/gr-overlay';
import '../gr-create-change-dialog/gr-create-change-dialog';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-repo-commands_html';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {customElement, property} from '@polymer/decorators';
import {
BranchName,
ConfigInfo,
@@ -41,8 +36,15 @@
firePageError,
fireTitleChange,
} from '../../../utils/event-util';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {ErrorCallback} from '../../../api/rest';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {subpageStyles} from '../../../styles/gr-subpage-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, css, html} from 'lit';
+import {customElement, query, property, state} from 'lit/decorators';
+import {assertIsDefined} from '../../../utils/common-util';
const GC_MESSAGE = 'Garbage collection completed successfully.';
const CONFIG_BRANCH = 'refs/meta/config' as BranchName;
@@ -52,80 +54,167 @@
const CREATE_CHANGE_FAILED_MESSAGE = 'Failed to create change.';
const CREATE_CHANGE_SUCCEEDED_MESSAGE = 'Navigating to change';
-export interface GrRepoCommands {
- $: {
- createChangeOverlay: GrOverlay;
- createNewChangeModal: GrCreateChangeDialog;
- };
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-repo-commands': GrRepoCommands;
+ }
}
@customElement('gr-repo-commands')
-export class GrRepoCommands extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
+export class GrRepoCommands extends LitElement {
+ @query('#createChangeOverlay')
+ private readonly createChangeOverlay?: GrOverlay;
- // This is a required property. Without `repo` being set the component is not
- // useful. Thus using !.
+ @query('#createNewChangeModal')
+ private readonly createNewChangeModal?: GrCreateChangeDialog;
+
@property({type: String})
- repo!: RepoName;
+ repo?: RepoName;
- @property({type: Boolean})
- _loading = true;
+ @state() private loading = true;
- @property({type: Object})
- _repoConfig?: ConfigInfo;
+ @state() private repoConfig?: ConfigInfo;
- @property({type: Boolean})
- _canCreate = false;
+ @state() private canCreateChange = false;
- @property({type: Boolean})
- _creatingChange = false;
+ @state() private creatingChange = false;
- @property({type: Boolean})
- _editingConfig = false;
+ @state() private editingConfig = false;
- @property({type: Boolean})
- _runningGC = false;
+ @state() private runningGC = false;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
override connectedCallback() {
super.connectedCallback();
- this._loadRepo();
-
fireTitleChange(this, 'Repo Commands');
}
- _loadRepo() {
+ static override get styles() {
+ return [
+ fontStyles,
+ formStyles,
+ subpageStyles,
+ sharedStyles,
+ css`
+ #form gr-button {
+ margin-bottom: var(--spacing-xxl);
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`
+ <div class="main gr-form-styles read-only">
+ <h1 id="Title" class="heading-1">Repository Commands</h1>
+ <div id="loading" class="${this.loading ? 'loading' : ''}">
+ Loading...
+ </div>
+ <div id="loadedContent" class="${this.loading ? 'loading' : ''}">
+ <h2 id="options" class="heading-2">Command</h2>
+ <div id="form">
+ <h3 class="heading-3">Create change</h3>
+ <gr-button
+ ?loading=${this.creatingChange}
+ @click=${() => {
+ this.createNewChange();
+ }}
+ >
+ Create change
+ </gr-button>
+ <h3 class="heading-3">Edit repo config</h3>
+ <gr-button
+ id="editRepoConfig"
+ ?loading=${this.editingConfig}
+ @click=${() => {
+ this.handleEditRepoConfig();
+ }}
+ >
+ Edit repo config
+ </gr-button>
+ ${this.renderRepoGarbageCollector()}
+ <gr-endpoint-decorator name="repo-command">
+ <gr-endpoint-param name="config" .value=${this.repoConfig}>
+ </gr-endpoint-param>
+ <gr-endpoint-param name="repoName" .value="${this.repo}">
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </div>
+ </div>
+ </div>
+ <gr-overlay id="createChangeOverlay" with-backdrop>
+ <gr-dialog
+ id="createChangeDialog"
+ confirm-label="Create"
+ ?disabled=${!this.canCreateChange}
+ @confirm=${() => {
+ this.handleCreateChange();
+ }}
+ @cancel=${() => {
+ this.handleCloseCreateChange();
+ }}
+ >
+ <div class="header" slot="header">Create Change</div>
+ <div class="main" slot="main">
+ <gr-create-change-dialog
+ id="createNewChangeModal"
+ .repoName="${this.repo}"
+ .privateByDefault="${this.repoConfig?.private_by_default}"
+ @can-create-change=${() => {
+ this.handleCanCreateChange();
+ }}
+ ></gr-create-change-dialog>
+ </div>
+ </gr-dialog>
+ </gr-overlay>
+ `;
+ }
+
+ private renderRepoGarbageCollector() {
+ if (!this.repoConfig?.actions || !this.repoConfig?.actions['gc']?.enabled)
+ return;
+
+ return html`
+ <h3 class="heading-3">${this.repoConfig?.actions['gc']?.label}</h3>
+ <gr-button
+ title="${this.repoConfig?.actions['gc']?.title || ''}"
+ ?loading=${this.runningGC}
+ @click=${() => this.handleRunningGC()}
+ >
+ ${this.repoConfig?.actions['gc']?.label}
+ </gr-button>
+ `;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('repo')) {
+ this.loadRepo();
+ }
+ }
+
+ // private but used in test
+ loadRepo() {
+ if (!this.repo) return;
+
const errFn: ErrorCallback = response => {
- // Do not process the error, if the component is not attached to the DOM
- // anymore, which at least in tests can happen.
- if (!this.isConnected) return;
firePageError(response);
};
- this.restApiService.getProjectConfig(this.repo, errFn).then(config => {
- if (!config) return;
- // Do not process the response, if the component is not attached to the
- // DOM anymore, which at least in tests can happen.
- if (!this.isConnected) return;
- this._repoConfig = config;
- this._loading = false;
- });
+ this.restApiService
+ .getProjectConfig(this.repo, errFn)
+ .then(config => {
+ if (!config) return;
+ this.repoConfig = config;
+ })
+ .finally(() => {
+ this.loading = false;
+ });
}
- _computeLoadingClass(loading: boolean) {
- return loading ? 'loading' : '';
- }
-
- _isLoading() {
- return this._loading;
- }
-
- _handleRunningGC() {
+ private handleRunningGC() {
if (!this.repo) return;
- this._runningGC = true;
+ this.runningGC = true;
return this.restApiService
.runRepoGC(this.repo)
.then(response => {
@@ -134,31 +223,40 @@
}
})
.finally(() => {
- this._runningGC = false;
+ this.runningGC = false;
});
}
- _createNewChange() {
- this.$.createChangeOverlay.open();
+ // private but used in test
+ createNewChange() {
+ assertIsDefined(this.createChangeOverlay, 'createChangeOverlay');
+ this.createChangeOverlay.open();
}
- _handleCreateChange() {
- this._creatingChange = true;
- this.$.createNewChangeModal.handleCreateChange().finally(() => {
- this._creatingChange = false;
+ // private but used in test
+ handleCreateChange() {
+ assertIsDefined(this.createNewChangeModal, 'createNewChangeModal');
+ this.creatingChange = true;
+ this.createNewChangeModal.handleCreateChange().finally(() => {
+ this.creatingChange = false;
});
- this._handleCloseCreateChange();
+ this.handleCloseCreateChange();
}
- _handleCloseCreateChange() {
- this.$.createChangeOverlay.close();
+ // private but used in test
+ handleCloseCreateChange() {
+ assertIsDefined(this.createChangeOverlay, 'createChangeOverlay');
+ this.createChangeOverlay.close();
}
/**
* Returns a Promise for testing.
+ *
+ * private but used in test
*/
- _handleEditRepoConfig() {
- this._editingConfig = true;
+ handleEditRepoConfig() {
+ if (!this.repo) return;
+ this.editingConfig = true;
return this.restApiService
.createChange(
this.repo,
@@ -182,13 +280,13 @@
);
})
.finally(() => {
- this._editingConfig = false;
+ this.editingConfig = false;
});
}
-}
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-repo-commands': GrRepoCommands;
+ private handleCanCreateChange() {
+ assertIsDefined(this.createNewChangeModal, 'createNewChangeModal');
+ this.canCreateChange =
+ !!this.createNewChangeModal.branch && !!this.createNewChangeModal.subject;
}
}
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.ts b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.ts
deleted file mode 100644
index 9948f8f..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-font-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-subpage-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-form-styles">
- #form gr-button {
- margin-bottom: var(--spacing-xxl);
- }
- </style>
- <div class="main gr-form-styles read-only">
- <h1 id="Title" class="heading-1">Repository Commands</h1>
- <div id="loading" class$="[[_computeLoadingClass(_loading)]]">
- Loading...
- </div>
- <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
- <h2 id="options" class="heading-2">Command</h2>
- <div id="form">
- <h3 class="heading-3">Create change</h3>
- <gr-button loading="[[_creatingChange]]" on-click="_createNewChange">
- Create change
- </gr-button>
- <h3 class="heading-3">Edit repo config</h3>
- <gr-button
- id="editRepoConfig"
- loading="[[_editingConfig]]"
- on-click="_handleEditRepoConfig"
- >
- Edit repo config
- </gr-button>
- <h3 class="heading-3" hidden="[[!_repoConfig.actions.gc.enabled]]">
- [[_repoConfig.actions.gc.label]]
- </h3>
- <gr-button
- hidden="[[!_repoConfig.actions.gc.enabled]]"
- title="[[_repoConfig.actions.gc.title]]"
- loading="[[_runningGC]]"
- on-click="_handleRunningGC"
- >
- [[_repoConfig.actions.gc.label]]
- </gr-button>
- <gr-endpoint-decorator name="repo-command">
- <gr-endpoint-param name="config" value="[[_repoConfig]]">
- </gr-endpoint-param>
- <gr-endpoint-param name="repoName" value="[[repo]]">
- </gr-endpoint-param>
- </gr-endpoint-decorator>
- </div>
- </div>
- </div>
- <gr-overlay id="createChangeOverlay" with-backdrop="">
- <gr-dialog
- id="createChangeDialog"
- confirm-label="Create"
- disabled="[[!_canCreate]]"
- on-confirm="_handleCreateChange"
- on-cancel="_handleCloseCreateChange"
- >
- <div class="header" slot="header">Create Change</div>
- <div class="main" slot="main">
- <gr-create-change-dialog
- id="createNewChangeModal"
- can-create="{{_canCreate}}"
- repo-name="[[repo]]"
- ></gr-create-change-dialog>
- </div>
- </gr-dialog>
- </gr-overlay>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.js
deleted file mode 100644
index ac48484..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.js
+++ /dev/null
@@ -1,142 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-repo-commands.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {
- addListenerForTest,
- mockPromise,
- stubRestApi,
-} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-repo-commands');
-
-suite('gr-repo-commands tests', () => {
- let element;
-
- let repoStub;
-
- setup(() => {
- element = basicFixture.instantiate();
- // Note that this probably does not achieve what it is supposed to, because
- // getProjectConfig() is called as soon as the element is attached, so
- // stubbing it here has not effect anymore.
- repoStub = stubRestApi('getProjectConfig').returns(Promise.resolve({}));
- });
-
- suite('create new change dialog', () => {
- test('_createNewChange opens modal', () => {
- const openStub = sinon.stub(element.$.createChangeOverlay, 'open');
- element._createNewChange();
- assert.isTrue(openStub.called);
- });
-
- test('_handleCreateChange called when confirm fired', () => {
- sinon.stub(element, '_handleCreateChange');
- element.$.createChangeDialog.dispatchEvent(
- new CustomEvent('confirm', {
- composed: true, bubbles: true,
- }));
- assert.isTrue(element._handleCreateChange.called);
- });
-
- test('_handleCloseCreateChange called when cancel fired', () => {
- sinon.stub(element, '_handleCloseCreateChange');
- element.$.createChangeDialog.dispatchEvent(
- new CustomEvent('cancel', {
- composed: true, bubbles: true,
- }));
- assert.isTrue(element._handleCloseCreateChange.called);
- });
- });
-
- suite('edit repo config', () => {
- let createChangeStub;
- let urlStub;
- let handleSpy;
- let alertStub;
-
- setup(() => {
- createChangeStub = stubRestApi('createChange');
- urlStub = sinon.stub(GerritNav, 'getEditUrlForDiff');
- sinon.stub(GerritNav, 'navigateToRelativeUrl');
- handleSpy = sinon.spy(element, '_handleEditRepoConfig');
- alertStub = sinon.stub();
- element.repo = 'test';
- element.addEventListener('show-alert', alertStub);
- });
-
- test('successful creation of change', () => {
- const change = {_number: '1'};
- createChangeStub.returns(Promise.resolve(change));
- MockInteractions.tap(element.$.editRepoConfig);
- assert.isTrue(element.$.editRepoConfig.loading);
- return handleSpy.lastCall.returnValue.then(() => {
- flush();
-
- assert.isTrue(alertStub.called);
- assert.equal(alertStub.lastCall.args[0].detail.message,
- 'Navigating to change');
- assert.isTrue(urlStub.called);
- assert.deepEqual(urlStub.lastCall.args,
- [change, 'project.config', 1]);
- assert.isFalse(element.$.editRepoConfig.loading);
- });
- });
-
- test('unsuccessful creation of change', () => {
- createChangeStub.returns(Promise.resolve(null));
- MockInteractions.tap(element.$.editRepoConfig);
- assert.isTrue(element.$.editRepoConfig.loading);
- return handleSpy.lastCall.returnValue.then(() => {
- flush();
-
- assert.isTrue(alertStub.called);
- assert.equal(alertStub.lastCall.args[0].detail.message,
- 'Failed to create change.');
- assert.isFalse(urlStub.called);
- assert.isFalse(element.$.editRepoConfig.loading);
- });
- });
- });
-
- suite('404', () => {
- test('fires page-error', async () => {
- repoStub.restore();
-
- element.repo = 'test';
-
- const response = {status: 404};
- stubRestApi('getProjectConfig').callsFake((repo, errFn) => {
- errFn(response);
- return Promise.resolve(undefined);
- });
-
- await flush();
- const promise = mockPromise();
- addListenerForTest(document, 'page-error', e => {
- assert.deepEqual(e.detail.response, response);
- promise.resolve();
- });
-
- element._loadRepo();
- await promise;
- });
- });
-});
-
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
new file mode 100644
index 0000000..42d3333
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.ts
@@ -0,0 +1,180 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-repo-commands.js';
+import {GrRepoCommands} from './gr-repo-commands';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {
+ addListenerForTest,
+ mockPromise,
+ queryAndAssert,
+ stubRestApi,
+} from '../../../test/test-utils';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
+import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
+import {PageErrorEvent} from '../../../types/events';
+import {RepoName} from '../../../types/common';
+import {GrButton} from '../../shared/gr-button/gr-button';
+
+const basicFixture = fixtureFromElement('gr-repo-commands');
+
+suite('gr-repo-commands tests', () => {
+ let element: GrRepoCommands;
+ let repoStub: sinon.SinonStub;
+
+ setup(async () => {
+ element = basicFixture.instantiate();
+ await element.updateComplete;
+ // Note that this probably does not achieve what it is supposed to, because
+ // getProjectConfig() is called as soon as the element is attached, so
+ // stubbing it here has not effect anymore.
+ repoStub = stubRestApi('getProjectConfig').returns(
+ Promise.resolve(undefined)
+ );
+ });
+
+ suite('create new change dialog', () => {
+ test('createNewChange opens modal', () => {
+ const openStub = sinon.stub(
+ queryAndAssert<GrOverlay>(element, '#createChangeOverlay'),
+ 'open'
+ );
+ element.createNewChange();
+ assert.isTrue(openStub.called);
+ });
+
+ test('handleCreateChange called when confirm fired', () => {
+ const handleCreateChangeStub = sinon.stub(element, 'handleCreateChange');
+ queryAndAssert<GrDialog>(element, '#createChangeDialog').dispatchEvent(
+ new CustomEvent('confirm', {
+ composed: true,
+ bubbles: true,
+ })
+ );
+ assert.isTrue(handleCreateChangeStub.called);
+ });
+
+ test('handleCloseCreateChange called when cancel fired', () => {
+ const handleCloseCreateChangeStub = sinon.stub(
+ element,
+ 'handleCloseCreateChange'
+ );
+ queryAndAssert<GrDialog>(element, '#createChangeDialog').dispatchEvent(
+ new CustomEvent('cancel', {
+ composed: true,
+ bubbles: true,
+ })
+ );
+ assert.isTrue(handleCloseCreateChangeStub.called);
+ });
+ });
+
+ suite('edit repo config', () => {
+ let createChangeStub: sinon.SinonStub;
+ let urlStub: sinon.SinonStub;
+ let handleSpy: sinon.SinonSpy;
+ let alertStub: sinon.SinonStub;
+
+ setup(() => {
+ createChangeStub = stubRestApi('createChange');
+ urlStub = sinon.stub(GerritNav, 'getEditUrlForDiff');
+ sinon.stub(GerritNav, 'navigateToRelativeUrl');
+ handleSpy = sinon.spy(element, 'handleEditRepoConfig');
+ alertStub = sinon.stub();
+ element.repo = 'test' as RepoName;
+ element.addEventListener('show-alert', alertStub);
+ });
+
+ test('successful creation of change', async () => {
+ const change = {_number: '1'};
+ createChangeStub.returns(Promise.resolve(change));
+ MockInteractions.tap(
+ queryAndAssert<GrButton>(element, '#editRepoConfig')
+ );
+ await element.updateComplete;
+ assert.isTrue(
+ queryAndAssert<GrButton>(element, '#editRepoConfig').loading
+ );
+
+ await handleSpy.lastCall.returnValue;
+ await element.updateComplete;
+
+ assert.isTrue(alertStub.called);
+ assert.equal(
+ alertStub.lastCall.args[0].detail.message,
+ 'Navigating to change'
+ );
+ assert.isTrue(urlStub.called);
+ assert.deepEqual(urlStub.lastCall.args, [change, 'project.config', 1]);
+ assert.isFalse(
+ queryAndAssert<GrButton>(element, '#editRepoConfig').loading
+ );
+ });
+
+ test('unsuccessful creation of change', async () => {
+ createChangeStub.returns(Promise.resolve(null));
+ MockInteractions.tap(
+ queryAndAssert<GrButton>(element, '#editRepoConfig')
+ );
+ await element.updateComplete;
+ assert.isTrue(
+ queryAndAssert<GrButton>(element, '#editRepoConfig').loading
+ );
+
+ await handleSpy.lastCall.returnValue;
+ await element.updateComplete;
+
+ assert.isTrue(alertStub.called);
+ assert.equal(
+ alertStub.lastCall.args[0].detail.message,
+ 'Failed to create change.'
+ );
+ assert.isFalse(urlStub.called);
+ assert.isFalse(
+ queryAndAssert<GrButton>(element, '#editRepoConfig').loading
+ );
+ });
+ });
+
+ suite('404', () => {
+ test('fires page-error', async () => {
+ repoStub.restore();
+
+ element.repo = 'test' as RepoName;
+
+ const response = {status: 404} as Response;
+ stubRestApi('getProjectConfig').callsFake((_repo, errFn) => {
+ if (errFn !== undefined) {
+ errFn(response);
+ }
+ return Promise.resolve(undefined);
+ });
+
+ await element.updateComplete;
+ const promise = mockPromise();
+ addListenerForTest(document, 'page-error', e => {
+ assert.deepEqual((e as PageErrorEvent).detail.response, response);
+ promise.resolve();
+ });
+
+ element.loadRepo();
+ await promise;
+ });
+ });
+});
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
index 7800653..3a37971 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
@@ -18,7 +18,7 @@
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {RepoName, DashboardId, DashboardInfo} from '../../../types/common';
import {firePageError} from '../../../utils/event-util';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {ErrorCallback} from '../../../api/rest';
import {sharedStyles} from '../../../styles/shared-styles';
import {tableStyles} from '../../../styles/gr-table-styles';
@@ -41,7 +41,7 @@
@property({type: Array})
_dashboards?: DashboardRef[];
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
static override get styles() {
return [
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 052e07a..6186573 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
@@ -16,9 +16,6 @@
*/
import '@polymer/iron-input/iron-input';
-import '../../../styles/gr-form-styles';
-import '../../../styles/gr-table-styles';
-import '../../../styles/shared-styles';
import '../../shared/gr-account-link/gr-account-link';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-date-formatter/gr-date-formatter';
@@ -27,106 +24,358 @@
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 {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-repo-detail-list_html';
import {encodeURL} from '../../../utils/url-util';
-import {customElement, property} from '@polymer/decorators';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {GrCreatePointerDialog} from '../gr-create-pointer-dialog/gr-create-pointer-dialog';
import {
- RepoName,
- ProjectInfo,
BranchInfo,
- GitRef,
- TagInfo,
GitPersonInfo,
+ GitRef,
+ ProjectInfo,
+ RepoName,
+ TagInfo,
+ WebLinkInfo,
} from '../../../types/common';
import {AppElementRepoParams} from '../../gr-app-types';
-import {PolymerDomRepeatEvent} from '../../../types/types';
import {RepoDetailView} from '../../core/gr-navigation/gr-navigation';
import {firePageError} from '../../../utils/event-util';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {ErrorCallback} from '../../../api/rest';
import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {tableStyles} from '../../../styles/gr-table-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, css, html} from 'lit';
+import {customElement, query, property, state} from 'lit/decorators';
+import {BindValueChangeEvent} from '../../../types/events';
+import {assertIsDefined} from '../../../utils/common-util';
+import {ifDefined} from 'lit/directives/if-defined';
const PGP_START = '-----BEGIN PGP SIGNATURE-----';
-export interface GrRepoDetailList {
- $: {
- overlay: GrOverlay;
- createOverlay: GrOverlay;
- createNewModal: GrCreatePointerDialog;
- };
-}
-
@customElement('gr-repo-detail-list')
-export class GrRepoDetailList extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
+export class GrRepoDetailList extends LitElement {
+ @query('#overlay') private readonly overlay?: GrOverlay;
- @property({type: Object, observer: '_paramsChanged'})
+ @query('#createOverlay') private readonly createOverlay?: GrOverlay;
+
+ @query('#createNewModal')
+ private readonly createNewModal?: GrCreatePointerDialog;
+
+ @property({type: Object})
params?: AppElementRepoParams;
- @property({type: String})
- detailType?: RepoDetailView.BRANCHES | RepoDetailView.TAGS;
+ // private but used in test
+ @state() detailType?: RepoDetailView.BRANCHES | RepoDetailView.TAGS;
- @property({type: Boolean})
- _editing = false;
+ // private but used in test
+ @state() isOwner = false;
- @property({type: Boolean})
- _isOwner = false;
+ @state() private loggedIn = false;
- @property({type: Boolean})
- _loggedIn = false;
+ @state() private offset = 0;
- @property({type: Number})
- _offset = 0;
+ // private but used in test
+ @state() repo?: RepoName;
- @property({type: String})
- _repo?: RepoName;
+ // private but used in test
+ @state() items?: BranchInfo[] | TagInfo[];
- @property({type: Array})
- _items?: BranchInfo[] | TagInfo[];
+ @state() private readonly itemsPerPage = 25;
- // _shownItems should be BranchInfo[] | TagInfo[],
- // but TS incorrectly assumes that in the loop for(const item of _shownItems)
- // item has type BranchInfo, not BranchInfo | TagInfo.
- @property({type: Array, computed: 'computeShownItems(_items)'})
- _shownItems?: Array<BranchInfo | TagInfo>;
+ @state() private loading = true;
- @property({type: Number})
- _itemsPerPage = 25;
+ @state() private filter?: string;
- @property({type: Boolean})
- _loading = true;
+ @state() private refName?: GitRef;
- @property({type: String})
- _filter?: string;
+ @state() private newItemName = false;
- @property({type: String})
- _refName?: GitRef;
+ // private but used in test
+ @state() isEditing = false;
- @property({type: Boolean})
- _hasNewItemName = false;
+ // private but used in test
+ @state() revisedRef?: GitRef;
- @property({type: Boolean})
- _isEditing = false;
+ private readonly restApiService = getAppContext().restApiService;
- @property({type: String})
- _revisedRef?: GitRef;
-
- private readonly restApiService = appContext.restApiService;
-
- _determineIfOwner(repo: RepoName) {
- return this.restApiService
- .getRepoAccess(repo)
- .then(access => (this._isOwner = !!access?.[repo]?.is_owner));
+ static override get styles() {
+ return [
+ formStyles,
+ tableStyles,
+ sharedStyles,
+ css`
+ .tags td.name {
+ min-width: 25em;
+ }
+ td.name,
+ td.revision,
+ td.message {
+ word-break: break-word;
+ }
+ td.revision.tags {
+ width: 27em;
+ }
+ td.message,
+ td.tagger {
+ max-width: 15em;
+ }
+ .editing .editItem {
+ display: inherit;
+ }
+ .editItem,
+ .editing .editBtn,
+ .canEdit .revisionNoEditing,
+ .editing .revisionWithEditing,
+ .revisionEdit,
+ .hideItem {
+ display: none;
+ }
+ .revisionEdit gr-button {
+ margin-left: var(--spacing-m);
+ }
+ .editBtn {
+ margin-left: var(--spacing-l);
+ }
+ .canEdit .revisionEdit {
+ align-items: center;
+ display: flex;
+ }
+ .deleteButton:not(.show) {
+ display: none;
+ }
+ .tagger.hide {
+ display: none;
+ }
+ `,
+ ];
}
- _paramsChanged(params?: AppElementRepoParams) {
- if (!params?.repo) {
+ override render() {
+ return html`
+ <gr-list-view
+ .createNew=${this.loggedIn}
+ .filter=${this.filter}
+ .itemsPerPage=${this.itemsPerPage}
+ .items=${this.items}
+ .loading=${this.loading}
+ .offset=${this.offset}
+ .path=${this.getPath(this.repo, this.detailType)}
+ @create-clicked=${() => {
+ this.handleCreateClicked();
+ }}
+ >
+ <table id="list" class="genericList gr-form-styles">
+ <tbody>
+ <tr class="headerRow">
+ <th class="name topHeader">Name</th>
+ <th class="revision topHeader">Revision</th>
+ <th
+ class="message topHeader ${this.detailType ===
+ RepoDetailView.BRANCHES
+ ? 'hideItem'
+ : ''}"
+ >
+ Message
+ </th>
+ <th
+ class="tagger topHeader ${this.detailType ===
+ RepoDetailView.BRANCHES
+ ? 'hideItem'
+ : ''}"
+ >
+ Tagger
+ </th>
+ <th class="repositoryBrowser topHeader">Repository Browser</th>
+ <th class="delete topHeader"></th>
+ </tr>
+ <tr
+ id="loading"
+ class="loadingMsg ${this.loading ? 'loading' : ''}"
+ >
+ <td>Loading...</td>
+ </tr>
+ </tbody>
+ <tbody class=${this.loading ? 'loading' : ''}>
+ ${this.items
+ ?.slice(0, SHOWN_ITEMS_COUNT)
+ .map((item, index) => this.renderItemList(item, index))}
+ </tbody>
+ </table>
+ <gr-overlay id="overlay" with-backdrop>
+ <gr-confirm-delete-item-dialog
+ class="confirmDialog"
+ .item=${this.refName}
+ .itemTypeName=${this.computeItemName(this.detailType)}
+ @confirm=${() => this.handleDeleteItemConfirm()}
+ @cancel=${() => {
+ this.handleConfirmDialogCancel();
+ }}
+ ></gr-confirm-delete-item-dialog>
+ </gr-overlay>
+ </gr-list-view>
+ <gr-overlay id="createOverlay" with-backdrop>
+ <gr-dialog
+ id="createDialog"
+ ?disabled=${!this.newItemName}
+ confirm-label="Create"
+ @confirm=${() => {
+ this.handleCreateItem();
+ }}
+ @cancel=${() => {
+ this.handleCloseCreate();
+ }}
+ >
+ <div class="header" slot="header">
+ Create ${this.computeItemName(this.detailType)}
+ </div>
+ <div class="main" slot="main">
+ <gr-create-pointer-dialog
+ id="createNewModal"
+ .detailType=${this.computeItemName(this.detailType)}
+ .itemDetail=${this.detailType}
+ .repoName=${this.repo}
+ @update-item-name=${() => {
+ this.handleUpdateItemName();
+ }}
+ ></gr-create-pointer-dialog>
+ </div>
+ </gr-dialog>
+ </gr-overlay>
+ `;
+ }
+
+ private renderItemList(item: BranchInfo | TagInfo, index: number) {
+ return html`
+ <tr class="table">
+ <td class="${this.detailType} name">
+ <a href=${ifDefined(this.computeFirstWebLink(item))}>
+ ${this.stripRefs(item.ref, this.detailType)}
+ </a>
+ </td>
+ <td
+ class="${this.detailType} revision ${this.computeCanEditClass(
+ item.ref,
+ this.detailType,
+ this.isOwner
+ )}"
+ >
+ <span class="revisionNoEditing"> ${item.revision} </span>
+ <span class="revisionEdit ${this.isEditing ? 'editing' : ''}">
+ <span class="revisionWithEditing"> ${item.revision} </span>
+ <gr-button
+ class="editBtn"
+ link
+ data-index=${index}
+ @click=${() => {
+ this.handleEditRevision(index);
+ }}
+ >
+ edit
+ </gr-button>
+ <iron-input
+ class="editItem"
+ .bindValue=${this.revisedRef}
+ @bind-value-changed=${this.handleRevisedRefBindValueChanged}
+ >
+ <input />
+ </iron-input>
+ <gr-button
+ class="cancelBtn editItem"
+ link
+ @click=${() => {
+ this.handleCancelRevision();
+ }}
+ >
+ Cancel
+ </gr-button>
+ <gr-button
+ class="saveBtn editItem"
+ link
+ data-index=${index}
+ ?disabled=${!this.revisedRef}
+ @click=${() => {
+ this.handleSaveRevision(index);
+ }}
+ >
+ Save
+ </gr-button>
+ </span>
+ </td>
+ <td
+ class="message ${this.detailType === RepoDetailView.BRANCHES
+ ? 'hideItem'
+ : ''}"
+ >
+ ${(item as TagInfo)?.message
+ ? (item as TagInfo).message?.split(PGP_START)[0]
+ : ''}
+ </td>
+ <td
+ class="tagger ${this.detailType === RepoDetailView.BRANCHES
+ ? 'hideItem'
+ : ''}"
+ >
+ ${this.renderTagger((item as TagInfo).tagger)}
+ </td>
+ <td class="repositoryBrowser">
+ ${this.computeWeblink(item).map(link => this.renderWeblink(link))}
+ </td>
+ <td class="delete">
+ <gr-button
+ class="deleteButton ${item.can_delete || this.isOwner
+ ? 'show'
+ : ''}"
+ link
+ data-index=${index}
+ @click=${() => {
+ this.handleDeleteItem(index);
+ }}
+ >
+ Delete
+ </gr-button>
+ </td>
+ </tr>
+ `;
+ }
+
+ private renderTagger(tagger?: GitPersonInfo) {
+ if (!tagger) return;
+
+ return html`
+ <div class="tagger">
+ <gr-account-link .account=${tagger}> </gr-account-link>
+ (<gr-date-formatter withTooltip .dateStr=${tagger.date}>
+ </gr-date-formatter
+ >)
+ </div>
+ `;
+ }
+
+ private renderWeblink(link: WebLinkInfo) {
+ return html`
+ <a href=${link.url} class="webLink" rel="noopener" target="_blank">
+ (${link.name})
+ </a>
+ `;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('params')) {
+ this.paramsChanged();
+ }
+ }
+
+ // private but used in test
+ determineIfOwner(repo: RepoName) {
+ return this.restApiService
+ .getRepoAccess(repo)
+ .then(access => (this.isOwner = !!access?.[repo]?.is_owner));
+ }
+
+ // private but used in test
+ paramsChanged() {
+ if (!this.params?.repo) {
return Promise.reject(new Error('undefined repo'));
}
@@ -134,40 +383,40 @@
// to false and polymer removes this component, hence check for params
if (
!(
- params?.detail === RepoDetailView.BRANCHES ||
- params?.detail === RepoDetailView.TAGS
+ this.params?.detail === RepoDetailView.BRANCHES ||
+ this.params?.detail === RepoDetailView.TAGS
)
) {
return;
}
- this._repo = params.repo;
+ this.repo = this.params.repo;
- this._getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
- if (loggedIn && this._repo) {
- this._determineIfOwner(this._repo);
+ this.getLoggedIn().then(loggedIn => {
+ this.loggedIn = loggedIn;
+ if (loggedIn && this.repo) {
+ this.determineIfOwner(this.repo);
}
});
- this.detailType = params.detail;
+ this.detailType = this.params.detail;
- this._filter = params?.filter ?? '';
- this._offset = Number(params?.offset ?? 0);
+ this.filter = this.params?.filter ?? '';
+ this.offset = Number(this.params?.offset ?? 0);
if (!this.detailType)
return Promise.reject(new Error('undefined detailType'));
- return this._getItems(
- this._filter,
- this._repo,
- this._itemsPerPage,
- this._offset,
+ return this.getItems(
+ this.filter,
+ this.repo,
+ this.itemsPerPage,
+ this.offset,
this.detailType
);
}
// TODO(TS) Move this to object for easier read, understand.
- _getItems(
+ private getItems(
filter: string | undefined,
repo: RepoName | undefined,
itemsPerPage: number,
@@ -177,9 +426,9 @@
if (filter === undefined || !repo || offset === undefined) {
return Promise.reject(new Error('filter or repo or offset undefined'));
}
- this._loading = true;
- this._items = [];
- flush();
+ this.loading = true;
+ this.items = [];
+
const errFn: ErrorCallback = response => {
firePageError(response);
};
@@ -188,52 +437,42 @@
return this.restApiService
.getRepoBranches(filter, repo, itemsPerPage, offset, errFn)
.then(items => {
- if (!items) {
- return;
- }
- this._items = items;
- this._loading = false;
+ this.items = items ?? [];
+ this.loading = false;
+ })
+ .finally(() => {
+ this.loading = false;
});
} else if (detailType === RepoDetailView.TAGS) {
return this.restApiService
.getRepoTags(filter, repo, itemsPerPage, offset, errFn)
.then(items => {
- if (!items) {
- return;
- }
- this._items = items;
- this._loading = false;
+ this.items = items ?? [];
+ })
+ .finally(() => {
+ this.loading = false;
});
}
return Promise.reject(new Error('unknown detail type'));
}
- _getPath(repo?: RepoName, detailType?: RepoDetailView) {
+ private getPath(repo?: RepoName, detailType?: RepoDetailView) {
return `/admin/repos/${encodeURL(repo ?? '', false)},${detailType}`;
}
- _computeWeblink(repo: ProjectInfo | BranchInfo | TagInfo) {
- if (!repo.web_links) {
- return '';
- }
+ private computeWeblink(repo: ProjectInfo | BranchInfo | TagInfo) {
+ if (!repo.web_links) return [];
const webLinks = repo.web_links;
- return webLinks.length ? webLinks : null;
+ return webLinks.length ? webLinks : [];
}
- _computeFirstWebLink(repo: ProjectInfo | BranchInfo | TagInfo) {
- const webLinks = this._computeWeblink(repo);
- return webLinks ? webLinks[0].url : null;
+ private computeFirstWebLink(repo: ProjectInfo | BranchInfo | TagInfo) {
+ const webLinks = this.computeWeblink(repo);
+ return webLinks.length > 0 ? webLinks[0].url : undefined;
}
- _computeMessage(message?: string) {
- if (!message) {
- return;
- }
- // Strip PGP info.
- return message.split(PGP_START)[0];
- }
-
- _stripRefs(item: GitRef, detailType?: RepoDetailView) {
+ // private but used in test
+ stripRefs(item: GitRef, detailType?: RepoDetailView) {
if (detailType === RepoDetailView.BRANCHES) {
return item.replace('refs/heads/', '');
} else if (detailType === RepoDetailView.TAGS) {
@@ -242,62 +481,61 @@
throw new Error('unknown detailType');
}
- _getLoggedIn() {
+ // private but used in test
+ getLoggedIn() {
return this.restApiService.getLoggedIn();
}
- _computeEditingClass(isEditing: boolean) {
- return isEditing ? 'editing' : '';
- }
-
- _computeCanEditClass(
+ private computeCanEditClass(
ref?: GitRef,
detailType?: RepoDetailView,
isOwner?: boolean
) {
if (ref === undefined || detailType === undefined) return '';
- return isOwner && this._stripRefs(ref, detailType) === 'HEAD'
+ return isOwner && this.stripRefs(ref, detailType) === 'HEAD'
? 'canEdit'
: '';
}
- _handleEditRevision(e: PolymerDomRepeatEvent<BranchInfo | TagInfo>) {
- this._revisedRef = e.model.get('item.revision') as unknown as GitRef;
- this._isEditing = true;
+ private handleEditRevision(index: number) {
+ if (!this.items) return;
+
+ this.revisedRef = this.items[index].revision as GitRef;
+ this.isEditing = true;
}
- _handleCancelRevision() {
- this._isEditing = false;
+ private handleCancelRevision() {
+ this.isEditing = false;
}
- _handleSaveRevision(e: PolymerDomRepeatEvent<BranchInfo | TagInfo>) {
- if (this._revisedRef && this._repo)
- this._setRepoHead(this._repo, this._revisedRef, e);
+ // private but used in test
+ handleSaveRevision(index: number) {
+ if (this.revisedRef && this.repo)
+ this.setRepoHead(this.repo, this.revisedRef, index);
}
- _setRepoHead(
- repo: RepoName,
- ref: GitRef,
- e: PolymerDomRepeatEvent<BranchInfo | TagInfo>
- ) {
+ // private but used in test
+ setRepoHead(repo: RepoName, ref: GitRef, index: number) {
+ if (!this.items) return;
return this.restApiService.setRepoHead(repo, ref).then(res => {
if (res.status < 400) {
- this._isEditing = false;
- e.model.set('item.revision', ref);
- // This is needed to refresh _items property with fresh data,
+ this.isEditing = false;
+ this.items![index].revision = ref;
+ // This is needed to refresh 'items' property with fresh data,
// specifically can_delete from the json response.
- this._getItems(
- this._filter,
- this._repo,
- this._itemsPerPage,
- this._offset,
+ this.getItems(
+ this.filter,
+ this.repo,
+ this.itemsPerPage,
+ this.offset,
this.detailType!
);
}
});
}
- _computeItemName(detailType?: RepoDetailView) {
+ // private but used in test
+ computeItemName(detailType?: RepoDetailView) {
if (detailType === undefined) return '';
if (detailType === RepoDetailView.BRANCHES) {
return 'Branch';
@@ -307,35 +545,36 @@
throw new Error('unknown detailType');
}
- _handleDeleteItemConfirm() {
- this.$.overlay.close();
- if (!this._repo || !this._refName) {
+ private handleDeleteItemConfirm() {
+ assertIsDefined(this.overlay, 'overlay');
+ this.overlay.close();
+ if (!this.repo || !this.refName) {
return Promise.reject(new Error('undefined repo or refName'));
}
if (this.detailType === RepoDetailView.BRANCHES) {
return this.restApiService
- .deleteRepoBranches(this._repo, this._refName)
+ .deleteRepoBranches(this.repo, this.refName)
.then(itemDeleted => {
if (itemDeleted.status === 204) {
- this._getItems(
- this._filter,
- this._repo,
- this._itemsPerPage,
- this._offset,
+ this.getItems(
+ this.filter,
+ this.repo,
+ this.itemsPerPage,
+ this.offset,
this.detailType!
);
}
});
} else if (this.detailType === RepoDetailView.TAGS) {
return this.restApiService
- .deleteRepoTags(this._repo, this._refName)
+ .deleteRepoTags(this.repo, this.refName)
.then(itemDeleted => {
if (itemDeleted.status === 204) {
- this._getItems(
- this._filter,
- this._repo,
- this._itemsPerPage,
- this._offset,
+ this.getItems(
+ this.filter,
+ this.repo,
+ this.itemsPerPage,
+ this.offset,
this.detailType!
);
}
@@ -344,61 +583,49 @@
return Promise.reject(new Error('unknown detail type'));
}
- _handleConfirmDialogCancel() {
- this.$.overlay.close();
+ private handleConfirmDialogCancel() {
+ assertIsDefined(this.overlay, 'overlay');
+ this.overlay.close();
}
- _handleDeleteItem(e: PolymerDomRepeatEvent<BranchInfo | TagInfo>) {
- const name = this._stripRefs(
- e.model.get('item.ref'),
+ private handleDeleteItem(index: number) {
+ if (!this.items) return;
+ assertIsDefined(this.overlay, 'overlay');
+ const name = this.stripRefs(
+ this.items[index].ref,
this.detailType
) as GitRef;
- if (!name) {
- return;
- }
- this._refName = name;
- this.$.overlay.open();
+ if (!name) return;
+ this.refName = name;
+ this.overlay.open();
}
- _computeHideDeleteClass(owner?: boolean, canDelete?: boolean) {
- if (canDelete || owner) {
- return 'show';
- }
-
- return '';
+ // private but used in test
+ handleCreateItem() {
+ assertIsDefined(this.createNewModal, 'createNewModal');
+ this.createNewModal.handleCreateItem();
+ this.handleCloseCreate();
}
- _handleCreateItem() {
- this.$.createNewModal.handleCreateItem();
- this._handleCloseCreate();
+ // private but used in test
+ handleCloseCreate() {
+ assertIsDefined(this.createOverlay, 'createOverlay');
+ this.createOverlay.close();
}
- _handleCloseCreate() {
- this.$.createOverlay.close();
+ // private but used in test
+ handleCreateClicked() {
+ assertIsDefined(this.createOverlay, 'createOverlay');
+ this.createOverlay.open();
}
- _handleCreateClicked() {
- this.$.createOverlay.open();
+ private handleUpdateItemName() {
+ assertIsDefined(this.createNewModal, 'createNewModal');
+ this.newItemName = !!this.createNewModal.itemName;
}
- _hideIfBranch(type?: RepoDetailView) {
- if (type === RepoDetailView.BRANCHES) {
- return 'hideItem';
- }
-
- return '';
- }
-
- _computeHideTagger(tagger?: GitPersonInfo) {
- return tagger ? '' : 'hide';
- }
-
- computeLoadingClass(loading: boolean) {
- return loading ? 'loading' : '';
- }
-
- computeShownItems(items: BranchInfo[] | TagInfo[]) {
- return items.slice(0, SHOWN_ITEMS_COUNT);
+ private handleRevisedRefBindValueChanged(e: BindValueChangeEvent) {
+ this.revisedRef = e.detail.value as GitRef;
}
}
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.ts b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.ts
deleted file mode 100644
index 429a6d6..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.ts
+++ /dev/null
@@ -1,214 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-form-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-table-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- .tags td.name {
- min-width: 25em;
- }
- td.name,
- td.revision,
- td.message {
- word-break: break-word;
- }
- td.revision.tags {
- width: 27em;
- }
- td.message,
- td.tagger {
- max-width: 15em;
- }
- .editing .editItem {
- display: inherit;
- }
- .editItem,
- .editing .editBtn,
- .canEdit .revisionNoEditing,
- .editing .revisionWithEditing,
- .revisionEdit,
- .hideItem {
- display: none;
- }
- .revisionEdit gr-button {
- margin-left: var(--spacing-m);
- }
- .editBtn {
- margin-left: var(--spacing-l);
- }
- .canEdit .revisionEdit {
- align-items: center;
- display: flex;
- }
- .deleteButton:not(.show) {
- display: none;
- }
- .tagger.hide {
- display: none;
- }
- </style>
- <style include="gr-table-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <gr-list-view
- create-new="[[_loggedIn]]"
- filter="[[_filter]]"
- items-per-page="[[_itemsPerPage]]"
- items="[[_items]]"
- loading="[[_loading]]"
- offset="[[_offset]]"
- on-create-clicked="_handleCreateClicked"
- path="[[_getPath(_repo, detailType)]]"
- >
- <table id="list" class="genericList gr-form-styles">
- <tbody>
- <tr class="headerRow">
- <th class="name topHeader">Name</th>
- <th class="revision topHeader">Revision</th>
- <th class$="message topHeader [[_hideIfBranch(detailType)]]">
- Message
- </th>
- <th class$="tagger topHeader [[_hideIfBranch(detailType)]]">
- Tagger
- </th>
- <th class="repositoryBrowser topHeader">Repository Browser</th>
- <th class="delete topHeader"></th>
- </tr>
- <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
- <td>Loading...</td>
- </tr>
- </tbody>
- <tbody class$="[[computeLoadingClass(_loading)]]">
- <template is="dom-repeat" items="[[_shownItems]]">
- <tr class="table">
- <td class$="[[detailType]] name">
- <a href$="[[_computeFirstWebLink(item)]]">
- [[_stripRefs(item.ref, detailType)]]
- </a>
- </td>
- <td
- class$="[[detailType]] revision [[_computeCanEditClass(item.ref, detailType, _isOwner)]]"
- >
- <span class="revisionNoEditing"> [[item.revision]] </span>
- <span class$="revisionEdit [[_computeEditingClass(_isEditing)]]">
- <span class="revisionWithEditing"> [[item.revision]] </span>
- <gr-button
- link=""
- on-click="_handleEditRevision"
- class="editBtn"
- >
- edit
- </gr-button>
- <iron-input bind-value="{{_revisedRef}}" class="editItem">
- <input is="iron-input" bind-value="{{_revisedRef}}" />
- </iron-input>
- <gr-button
- link=""
- on-click="_handleCancelRevision"
- class="cancelBtn editItem"
- >
- Cancel
- </gr-button>
- <gr-button
- link=""
- on-click="_handleSaveRevision"
- class="saveBtn editItem"
- disabled="[[!_revisedRef]]"
- >
- Save
- </gr-button>
- </span>
- </td>
- <td class$="message [[_hideIfBranch(detailType)]]">
- [[_computeMessage(item.message)]]
- </td>
- <td class$="tagger [[_hideIfBranch(detailType)]]">
- <div class$="tagger [[_computeHideTagger(item.tagger)]]">
- <gr-account-link account="[[item.tagger]]"> </gr-account-link>
- (<gr-date-formatter withTooltip date-str="[[item.tagger.date]]">
- </gr-date-formatter
- >)
- </div>
- </td>
- <td class="repositoryBrowser">
- <template
- is="dom-repeat"
- items="[[_computeWeblink(item)]]"
- as="link"
- >
- <a
- href$="[[link.url]]"
- class="webLink"
- rel="noopener"
- target="_blank"
- >
- ([[link.name]])
- </a>
- </template>
- </td>
- <td class="delete">
- <gr-button
- link=""
- class$="deleteButton [[_computeHideDeleteClass(_isOwner, item.can_delete)]]"
- on-click="_handleDeleteItem"
- >
- Delete
- </gr-button>
- </td>
- </tr>
- </template>
- </tbody>
- </table>
- <gr-overlay id="overlay" with-backdrop="">
- <gr-confirm-delete-item-dialog
- class="confirmDialog"
- on-confirm="_handleDeleteItemConfirm"
- on-cancel="_handleConfirmDialogCancel"
- item="[[_refName]]"
- item-type-name="[[_computeItemName(detailType)]]"
- ></gr-confirm-delete-item-dialog>
- </gr-overlay>
- </gr-list-view>
- <gr-overlay id="createOverlay" with-backdrop="">
- <gr-dialog
- id="createDialog"
- disabled="[[!_hasNewItemName]]"
- confirm-label="Create"
- on-confirm="_handleCreateItem"
- on-cancel="_handleCloseCreate"
- >
- <div class="header" slot="header">
- Create [[_computeItemName(detailType)]]
- </div>
- <div class="main" slot="main">
- <gr-create-pointer-dialog
- id="createNewModal"
- detail-type="[[_computeItemName(detailType)]]"
- has-new-item-name="{{_hasNewItemName}}"
- item-detail="[[detailType]]"
- repo-name="[[_repo]]"
- ></gr-create-pointer-dialog>
- </div>
- </gr-dialog>
- </gr-overlay>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.js b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.js
deleted file mode 100644
index d5eb5d5..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.js
+++ /dev/null
@@ -1,503 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-repo-detail-list.js';
-import 'lodash/lodash.js';
-import {page} from '../../../utils/page-wrapper-utils.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {
- addListenerForTest,
- mockPromise,
- stubRestApi,
-} from '../../../test/test-utils.js';
-import {RepoDetailView} from '../../core/gr-navigation/gr-navigation.js';
-
-const basicFixture = fixtureFromElement('gr-repo-detail-list');
-
-let counter;
-const branchGenerator = () => {
- return {
- ref: `refs/heads/test${++counter}`,
- revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d',
- web_links: [
- {
- name: 'diffusion',
- url: `https://git.example.org/branch/test;refs/heads/test${counter}`,
- },
- ],
- };
-};
-const tagGenerator = () => {
- return {
- ref: `refs/tags/test${++counter}`,
- revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d',
- web_links: [
- {
- name: 'diffusion',
- url: `https://git.example.org/tag/test;refs/tags/test${counter}`,
- },
- ],
- message: 'Annotated tag',
- tagger: {
- name: 'Test User',
- email: 'test.user@gmail.com',
- date: '2017-09-19 14:54:00.000000000',
- tz: 540,
- },
- };
-};
-
-suite('gr-repo-detail-list', () => {
- suite('Branches', () => {
- let element;
- let branches;
-
- setup(() => {
- element = basicFixture.instantiate();
- element.detailType = 'branches';
- counter = 0;
- sinon.stub(page, 'show');
- });
-
- suite('list of repo branches', () => {
- setup(async () => {
- branches = [{
- ref: 'HEAD',
- revision: 'master',
- }].concat(_.times(25, branchGenerator));
- stubRestApi('getRepoBranches').returns(Promise.resolve(branches));
-
- const params = {
- repo: 'test',
- detail: 'branches',
- };
- await element._paramsChanged(params);
- await flush();
- });
-
- test('test for branch in the list', () => {
- assert.equal(element._items[2].ref, 'refs/heads/test2');
- });
-
- test('test for web links in the branches list', () => {
- assert.equal(element._items[2].web_links[0].url,
- 'https://git.example.org/branch/test;refs/heads/test2');
- });
-
- test('test for refs/heads/ being striped from ref', () => {
- assert.equal(element._stripRefs(element._items[2].ref,
- element.detailType), 'test2');
- });
-
- test('_shownItems', () => {
- assert.equal(element._shownItems.length, 25);
- });
-
- test('Edit HEAD button not admin', async () => {
- sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
- stubRestApi('getRepoAccess').returns(
- Promise.resolve({
- test: {is_owner: false},
- }));
- await element._determineIfOwner('test');
- assert.equal(element._isOwner, false);
- assert.equal(getComputedStyle(dom(element.root)
- .querySelector('.revisionNoEditing')).display, 'inline');
- assert.equal(getComputedStyle(dom(element.root)
- .querySelector('.revisionEdit')).display, 'none');
- });
-
- test('Edit HEAD button admin', async () => {
- const saveBtn = element.root.querySelector('.saveBtn');
- const cancelBtn = element.root.querySelector('.cancelBtn');
- const editBtn = element.root.querySelector('.editBtn');
- const revisionNoEditing = dom(element.root)
- .querySelector('.revisionNoEditing');
- const revisionWithEditing = dom(element.root)
- .querySelector('.revisionWithEditing');
-
- sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
- stubRestApi('getRepoAccess').returns(
- Promise.resolve({
- test: {is_owner: true},
- }));
- sinon.stub(element, '_handleSaveRevision');
- await element._determineIfOwner('test');
- assert.equal(element._isOwner, true);
- // The revision container for non-editing enabled row is not visible.
- assert.equal(getComputedStyle(revisionNoEditing).display, 'none');
-
- // The revision container for editing enabled row is visible.
- assert.notEqual(getComputedStyle(dom(element.root)
- .querySelector('.revisionEdit')).display, 'none');
-
- // The revision and edit button are visible.
- assert.notEqual(getComputedStyle(revisionWithEditing).display,
- 'none');
- assert.notEqual(getComputedStyle(editBtn).display, 'none');
-
- // The input, cancel, and save buttons are not visible.
- const hiddenElements = dom(element.root)
- .querySelectorAll('.canEdit .editItem');
-
- for (const item of hiddenElements) {
- assert.equal(getComputedStyle(item).display, 'none');
- }
-
- MockInteractions.tap(editBtn);
- await flush();
- // The revision and edit button are not visible.
- assert.equal(getComputedStyle(revisionWithEditing).display, 'none');
- assert.equal(getComputedStyle(editBtn).display, 'none');
-
- // The input, cancel, and save buttons are not visible.
- for (const item of hiddenElements) {
- assert.notEqual(getComputedStyle(item).display, 'none');
- }
-
- // The revised ref was set correctly
- assert.equal(element._revisedRef, 'master');
-
- assert.isFalse(saveBtn.disabled);
-
- // Delete the ref.
- element._revisedRef = '';
- assert.isTrue(saveBtn.disabled);
-
- // Change the ref to something else
- element._revisedRef = 'newRef';
- element._repo = 'test';
- assert.isFalse(saveBtn.disabled);
-
- // Save button calls handleSave. since this is stubbed, the edit
- // section remains open.
- MockInteractions.tap(saveBtn);
- assert.isTrue(element._handleSaveRevision.called);
-
- // When cancel is tapped, the edit secion closes.
- MockInteractions.tap(cancelBtn);
- await flush();
-
- // The revision and edit button are visible.
- assert.notEqual(getComputedStyle(revisionWithEditing).display,
- 'none');
- assert.notEqual(getComputedStyle(editBtn).display, 'none');
-
- // The input, cancel, and save buttons are not visible.
- for (const item of hiddenElements) {
- assert.equal(getComputedStyle(item).display, 'none');
- }
- });
-
- test('_handleSaveRevision with invalid rev', async () => {
- const event = {model: {set: sinon.stub()}};
- element._isEditing = true;
- stubRestApi('setRepoHead').returns(
- Promise.resolve({
- status: 400,
- })
- );
-
- await element._setRepoHead('test', 'newRef', event);
- assert.isTrue(element._isEditing);
- assert.isFalse(event.model.set.called);
- });
-
- test('_handleSaveRevision with valid rev', async () => {
- const event = {model: {set: sinon.stub()}};
- element._isEditing = true;
- stubRestApi('setRepoHead').returns(
- Promise.resolve({
- status: 200,
- })
- );
-
- await element._setRepoHead('test', 'newRef', event);
- assert.isFalse(element._isEditing);
- assert.isTrue(event.model.set.called);
- });
-
- test('test _computeItemName', () => {
- assert.deepEqual(element._computeItemName('branches'), 'Branch');
- assert.deepEqual(element._computeItemName('tags'), 'Tag');
- });
- });
-
- suite('list with less then 25 branches', () => {
- setup(async () => {
- branches = _.times(25, branchGenerator);
- stubRestApi('getRepoBranches').returns(Promise.resolve(branches));
-
- const params = {
- repo: 'test',
- detail: 'branches',
- };
-
- await element._paramsChanged(params);
- await flush();
- });
-
- test('_shownItems', () => {
- assert.equal(element._shownItems.length, 25);
- });
- });
-
- suite('filter', () => {
- test('_paramsChanged', async () => {
- const stub = stubRestApi('getRepoBranches').returns(
- Promise.resolve(branches));
- const params = {
- detail: 'branches',
- repo: 'test',
- filter: 'test',
- offset: 25,
- };
- await element._paramsChanged(params);
- assert.equal(stub.lastCall.args[0], 'test');
- assert.equal(stub.lastCall.args[1], 'test');
- assert.equal(stub.lastCall.args[2], 25);
- assert.equal(stub.lastCall.args[3], 25);
- });
- });
-
- suite('404', () => {
- test('fires page-error', async () => {
- const response = {status: 404};
- stubRestApi('getRepoBranches').callsFake(
- (filter, repo, reposBranchesPerPage, opt_offset, errFn) => {
- errFn(response);
- return Promise.resolve();
- });
-
- const promise = mockPromise();
- addListenerForTest(document, 'page-error', e => {
- assert.deepEqual(e.detail.response, response);
- promise.resolve();
- });
-
- const params = {
- detail: 'branches',
- repo: 'test',
- filter: 'test',
- offset: 25,
- };
- element._paramsChanged(params);
- await promise;
- });
- });
- });
-
- suite('Tags', () => {
- let element;
- let tags;
-
- setup(() => {
- element = basicFixture.instantiate();
- element.detailType = 'tags';
- counter = 0;
- sinon.stub(page, 'show');
- });
-
- test('_computeMessage', () => {
- let message = 'v2.15-rc1↵-----BEGIN PGP SIGNATURE-----↵Version: GnuPG v' +
- '1↵↵iQIcBAABAgAGBQJZ27O7AAoJEF/XxZqaEoiMy6kQAMoQCpGr3J6JITI4BVWsr7QM↵xy' +
- 'EcWH5YPUko5EPTbkABHmaVyFmKGkuIQdn6c+NIbqJOk+5XT4oUyRSo1T569HPJ↵3kyxEJi' +
- 'T1ryvp5BIHwdvHx58fjw1+YkiWLZuZq1FFkUYqnWTYCrkv7Fok98pdOmV↵CL1Hgugi5uK8' +
- '/kxf1M7+Nv6piaZ140pwSb1h6QdAjaZVfaBCnoxlG4LRUqHvEYay↵f4QYgFT67auHIGkZ4' +
- 'moUcsp2Du/1jSsCWL/CPwjPFGbbckVAjLCMT9yD3NKwpEZF↵pfsiZyHI9dL0M+QjVrM+RD' +
- 'HwIIJwra8R0IMkDlQ6MDrFlKNqNBbo588S6UPrm71L↵YuiwWlcrK9ZIybxT6LzbR65Rvez' +
- 'DSitQ+xeIfpZE19/X6BCnvlARLE8k/tC2JksI↵lEZi7Lf3FQdIcwwyt98tJkS9HX9v9jbC' +
- '5QXifnoj3Li8tHSLuQ1dJCxHQiis6ojI↵OWUFkm0IHBXVNHA2dqYBdM+pL12mlI3wp6Ica' +
- '4cdEVDwzu+j1xnVSFUa+d+Y2xJF↵7mytuyhHiKG4hm+zbhMv6WD8Q3FoDsJZeLY99l0hYQ' +
- 'SnnkMduFVroIs45pAs8gUA↵RvYla8mm9w/543IJAPzzFarPVLSsSyQ7tJl3UBzjKRNH/rX' +
- 'W+F22qyWD1zyHPUIR↵C00ItmwlAvveImYKpQAH↵=L+K9↵-----END PGP SIGNATURE---' +
- '--';
- assert.equal(element._computeMessage(message), 'v2.15-rc1↵');
- message = 'v2.15-rc1';
- assert.equal(element._computeMessage(message), 'v2.15-rc1');
- });
-
- suite('list of repo tags', () => {
- setup(async () => {
- tags = _.times(26, tagGenerator);
- stubRestApi('getRepoTags').returns(Promise.resolve(tags));
-
- const params = {
- repo: 'test',
- detail: 'tags',
- };
-
- await element._paramsChanged(params);
- await flush();
- });
-
- test('test for tag in the list', async () => {
- assert.equal(element._items[1].ref, 'refs/tags/test2');
- });
-
- test('test for tag message in the list', async () => {
- assert.equal(element._items[1].message, 'Annotated tag');
- });
-
- test('test for tagger in the tag list', async () => {
- const tagger = {
- name: 'Test User',
- email: 'test.user@gmail.com',
- date: '2017-09-19 14:54:00.000000000',
- tz: 540,
- };
-
- assert.deepEqual(element._items[1].tagger, tagger);
- });
-
- test('test for web links in the tags list', async () => {
- assert.equal(element._items[1].web_links[0].url,
- 'https://git.example.org/tag/test;refs/tags/test2');
- });
-
- test('test for refs/tags/ being striped from ref', async () => {
- assert.equal(element._stripRefs(element._items[1].ref,
- element.detailType), 'test2');
- });
-
- test('_shownItems', () => {
- assert.equal(element._shownItems.length, 25);
- });
-
- test('_computeHideTagger', () => {
- const testObject1 = {
- tagger: 'test',
- };
- assert.equal(element._computeHideTagger(testObject1), '');
-
- assert.equal(element._computeHideTagger(undefined), 'hide');
- });
- });
-
- suite('list with less then 25 tags', () => {
- setup(async () => {
- tags = _.times(25, tagGenerator);
- stubRestApi('getRepoTags').returns(Promise.resolve(tags));
-
- const params = {
- repo: 'test',
- detail: 'tags',
- };
-
- await element._paramsChanged(params);
- await flush();
- });
-
- test('_shownItems', () => {
- assert.equal(element._shownItems.length, 25);
- });
- });
-
- suite('filter', () => {
- test('_paramsChanged', async () => {
- const stub = stubRestApi('getRepoTags').returns(Promise.resolve(tags));
- const params = {
- repo: 'test',
- detail: 'tags',
- filter: 'test',
- offset: 25,
- };
- await element._paramsChanged(params);
- assert.equal(stub.lastCall.args[0], 'test');
- assert.equal(stub.lastCall.args[1], 'test');
- assert.equal(stub.lastCall.args[2], 25);
- assert.equal(stub.lastCall.args[3], 25);
- });
- });
-
- suite('create new', () => {
- test('_handleCreateClicked called when create-click fired', () => {
- sinon.stub(element, '_handleCreateClicked');
- element.shadowRoot
- .querySelector('gr-list-view').dispatchEvent(
- new CustomEvent('create-clicked', {
- composed: true, bubbles: true,
- }));
- assert.isTrue(element._handleCreateClicked.called);
- });
-
- test('_handleCreateClicked opens modal', () => {
- const openStub = sinon.stub(element.$.createOverlay, 'open');
- element._handleCreateClicked();
- assert.isTrue(openStub.called);
- });
-
- test('_handleCreateItem called when confirm fired', () => {
- sinon.stub(element, '_handleCreateItem');
- element.$.createDialog.dispatchEvent(
- new CustomEvent('confirm', {
- composed: true, bubbles: true,
- }));
- assert.isTrue(element._handleCreateItem.called);
- });
-
- test('_handleCloseCreate called when cancel fired', () => {
- sinon.stub(element, '_handleCloseCreate');
- element.$.createDialog.dispatchEvent(
- new CustomEvent('cancel', {
- composed: true, bubbles: true,
- }));
- assert.isTrue(element._handleCloseCreate.called);
- });
- });
-
- suite('404', () => {
- test('fires page-error', async () => {
- const response = {status: 404};
- stubRestApi('getRepoTags').callsFake(
- (filter, repo, reposTagsPerPage, opt_offset, errFn) => {
- errFn(response);
- return Promise.resolve();
- });
-
- const promise = mockPromise();
- addListenerForTest(document, 'page-error', e => {
- assert.deepEqual(e.detail.response, response);
- promise.resolve();
- });
-
- const params = {
- repo: 'test',
- detail: 'tags',
- filter: 'test',
- offset: 25,
- };
- element._paramsChanged(params);
- await promise;
- });
- });
-
- test('test _computeHideDeleteClass', () => {
- assert.deepEqual(element._computeHideDeleteClass(true, false), 'show');
- assert.deepEqual(element._computeHideDeleteClass(false, true), 'show');
- assert.deepEqual(element._computeHideDeleteClass(false, false), '');
- });
-
- test('_computeItemName', () => {
- assert.equal(element._computeItemName(RepoDetailView.BRANCHES), 'Branch');
- assert.equal(element._computeItemName(RepoDetailView.TAGS),
- 'Tag');
- });
- });
-});
-
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
new file mode 100644
index 0000000..a82c4e3
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.ts
@@ -0,0 +1,602 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-repo-detail-list.js';
+import {GrRepoDetailList} from './gr-repo-detail-list';
+import {page} from '../../../utils/page-wrapper-utils';
+import {
+ addListenerForTest,
+ mockPromise,
+ queryAll,
+ queryAndAssert,
+ stubRestApi,
+} from '../../../test/test-utils';
+import {RepoDetailView} from '../../core/gr-navigation/gr-navigation';
+import {
+ BranchInfo,
+ EmailAddress,
+ GitRef,
+ GroupId,
+ GroupName,
+ ProjectAccessGroups,
+ ProjectAccessInfoMap,
+ RepoName,
+ TagInfo,
+ Timestamp,
+ TimezoneOffset,
+} from '../../../types/common';
+import {GerritView} from '../../../services/router/router-model';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+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';
+
+const basicFixture = fixtureFromElement('gr-repo-detail-list');
+
+function branchGenerator(counter: number) {
+ return {
+ ref: `refs/heads/test${counter}` as GitRef,
+ revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d',
+ web_links: [
+ {
+ name: 'diffusion',
+ url: `https://git.example.org/branch/test;refs/heads/test${counter}`,
+ },
+ ],
+ };
+}
+
+function createBranchesList(n: number) {
+ const branches = [];
+ for (let i = 0; i < n; ++i) {
+ branches.push(branchGenerator(i));
+ }
+ return branches;
+}
+
+function tagGenerator(counter: number) {
+ return {
+ ref: `refs/tags/test${counter}` as GitRef,
+ revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d',
+ can_delete: false,
+ web_links: [
+ {
+ name: 'diffusion',
+ url: `https://git.example.org/tag/test;refs/tags/test${counter}`,
+ },
+ ],
+ message: 'Annotated tag',
+ tagger: {
+ name: 'Test User',
+ email: 'test.user@gmail.com' as EmailAddress,
+ date: '2017-09-19 14:54:00.000000000' as Timestamp,
+ tz: 540 as TimezoneOffset,
+ },
+ };
+}
+
+function createTagsList(n: number) {
+ const tags = [];
+ for (let i = 0; i < n; ++i) {
+ tags.push(tagGenerator(i));
+ }
+ return tags;
+}
+
+suite('gr-repo-detail-list', () => {
+ suite('Branches', () => {
+ let element: GrRepoDetailList;
+ let branches: BranchInfo[];
+
+ setup(async () => {
+ element = basicFixture.instantiate();
+ await element.updateComplete;
+ element.detailType = RepoDetailView.BRANCHES;
+ sinon.stub(page, 'show');
+ });
+
+ suite('list of repo branches', () => {
+ setup(async () => {
+ branches = [
+ {
+ ref: 'HEAD' as GitRef,
+ revision: 'master',
+ },
+ ].concat(createBranchesList(25));
+ stubRestApi('getRepoBranches').returns(Promise.resolve(branches));
+
+ element.params = {
+ view: GerritView.REPO,
+ repo: 'test' as RepoName,
+ detail: RepoDetailView.BRANCHES,
+ };
+ await element.paramsChanged();
+ await element.updateComplete;
+ });
+
+ test('test for branch in the list', () => {
+ assert.equal(element.items![3].ref, 'refs/heads/test2');
+ });
+
+ test('test for web links in the branches list', () => {
+ assert.equal(
+ element.items![3].web_links![0].url,
+ 'https://git.example.org/branch/test;refs/heads/test2'
+ );
+ });
+
+ test('test for refs/heads/ being striped from ref', () => {
+ assert.equal(
+ element.stripRefs(element.items![3].ref, element.detailType),
+ 'test2'
+ );
+ });
+
+ test('items', () => {
+ assert.equal(queryAll<HTMLTableElement>(element, '.table').length, 25);
+ });
+
+ test('Edit HEAD button not admin', async () => {
+ sinon.stub(element, 'getLoggedIn').returns(Promise.resolve(true));
+ stubRestApi('getRepoAccess').returns(
+ Promise.resolve({
+ test: {
+ revision: 'xxxx',
+ local: {
+ 'refs/*': {
+ permissions: {
+ owner: {rules: {xxx: {action: 'ALLOW', force: false}}},
+ },
+ },
+ },
+ owner_of: ['refs/*'] as GitRef[],
+ groups: {
+ xxxx: {
+ id: 'xxxx' as GroupId,
+ url: 'test',
+ name: 'test' as GroupName,
+ },
+ } as ProjectAccessGroups,
+ config_web_links: [{name: 'gitiles', url: 'test'}],
+ },
+ } as ProjectAccessInfoMap)
+ );
+ await element.determineIfOwner('test' as RepoName);
+ assert.equal(element.isOwner, false);
+ assert.equal(
+ getComputedStyle(
+ queryAndAssert<HTMLSpanElement>(element, '.revisionNoEditing')
+ ).display,
+ 'inline'
+ );
+ assert.equal(
+ getComputedStyle(
+ queryAndAssert<HTMLSpanElement>(element, '.revisionEdit')
+ ).display,
+ 'none'
+ );
+ });
+
+ test('Edit HEAD button admin', async () => {
+ const saveBtn = queryAndAssert<GrButton>(element, '.saveBtn');
+ const cancelBtn = queryAndAssert<GrButton>(element, '.cancelBtn');
+ const editBtn = queryAndAssert<GrButton>(element, '.editBtn');
+ const revisionNoEditing = queryAndAssert<HTMLSpanElement>(
+ element,
+ '.revisionNoEditing'
+ );
+ const revisionWithEditing = queryAndAssert<HTMLSpanElement>(
+ element,
+ '.revisionWithEditing'
+ );
+
+ sinon.stub(element, 'getLoggedIn').returns(Promise.resolve(true));
+ stubRestApi('getRepoAccess').returns(
+ Promise.resolve({
+ test: {
+ revision: 'xxxx',
+ local: {
+ 'refs/*': {
+ permissions: {
+ owner: {rules: {xxx: {action: 'ALLOW', force: false}}},
+ },
+ },
+ },
+ is_owner: true,
+ owner_of: ['refs/*'] as GitRef[],
+ groups: {
+ xxxx: {
+ id: 'xxxx' as GroupId,
+ url: 'test',
+ name: 'test' as GroupName,
+ },
+ } as ProjectAccessGroups,
+ config_web_links: [{name: 'gitiles', url: 'test'}],
+ },
+ } as ProjectAccessInfoMap)
+ );
+ const handleSaveRevisionStub = sinon.stub(
+ element,
+ 'handleSaveRevision'
+ );
+ await element.determineIfOwner('test' as RepoName);
+ assert.equal(element.isOwner, true);
+ // The revision container for non-editing enabled row is not visible.
+ assert.equal(getComputedStyle(revisionNoEditing).display, 'none');
+
+ // The revision container for editing enabled row is visible.
+ assert.notEqual(
+ getComputedStyle(
+ queryAndAssert<HTMLSpanElement>(element, '.revisionEdit')
+ ).display,
+ 'none'
+ );
+
+ // The revision and edit button are visible.
+ assert.notEqual(getComputedStyle(revisionWithEditing).display, 'none');
+ assert.notEqual(getComputedStyle(editBtn).display, 'none');
+
+ // The input, cancel, and save buttons are not visible.
+ const hiddenElements = queryAll<HTMLTableElement>(
+ element,
+ '.canEdit .editItem'
+ );
+
+ for (const item of hiddenElements) {
+ assert.equal(getComputedStyle(item).display, 'none');
+ }
+
+ MockInteractions.tap(editBtn);
+ await element.updateComplete;
+ // The revision and edit button are not visible.
+ assert.equal(getComputedStyle(revisionWithEditing).display, 'none');
+ assert.equal(getComputedStyle(editBtn).display, 'none');
+
+ // The input, cancel, and save buttons are not visible.
+ for (const item of hiddenElements) {
+ assert.notEqual(getComputedStyle(item).display, 'none');
+ }
+
+ // The revised ref was set correctly
+ assert.equal(element.revisedRef, 'master' as GitRef);
+
+ assert.isFalse(saveBtn.disabled);
+
+ // Delete the ref.
+ element.revisedRef = '' as GitRef;
+ await element.updateComplete;
+ assert.isTrue(saveBtn.disabled);
+
+ // Change the ref to something else
+ element.revisedRef = 'newRef' as GitRef;
+ element.repo = 'test' as RepoName;
+ await element.updateComplete;
+ assert.isFalse(saveBtn.disabled);
+
+ // Save button calls handleSave. since this is stubbed, the edit
+ // section remains open.
+ MockInteractions.tap(saveBtn);
+ assert.isTrue(handleSaveRevisionStub.called);
+
+ // When cancel is tapped, the edit secion closes.
+ MockInteractions.tap(cancelBtn);
+ await element.updateComplete;
+
+ // The revision and edit button are visible.
+ assert.notEqual(getComputedStyle(revisionWithEditing).display, 'none');
+ assert.notEqual(getComputedStyle(editBtn).display, 'none');
+
+ // The input, cancel, and save buttons are not visible.
+ for (const item of hiddenElements) {
+ assert.equal(getComputedStyle(item).display, 'none');
+ }
+ });
+
+ test('handleSaveRevision with invalid rev', async () => {
+ element.isEditing = true;
+ stubRestApi('setRepoHead').returns(
+ Promise.resolve({
+ status: 400,
+ } as Response)
+ );
+
+ await element.setRepoHead('test' as RepoName, 'newRef' as GitRef, 1);
+ assert.isTrue(element.isEditing);
+ });
+
+ test('handleSaveRevision with valid rev', async () => {
+ element.isEditing = true;
+ stubRestApi('setRepoHead').returns(
+ Promise.resolve({
+ status: 200,
+ } as Response)
+ );
+
+ await element.setRepoHead('test' as RepoName, 'newRef' as GitRef, 1);
+ assert.isFalse(element.isEditing);
+ });
+
+ test('test computeItemName', () => {
+ assert.deepEqual(
+ element.computeItemName(RepoDetailView.BRANCHES),
+ 'Branch'
+ );
+ assert.deepEqual(element.computeItemName(RepoDetailView.TAGS), 'Tag');
+ });
+ });
+
+ suite('list with less then 25 branches', () => {
+ setup(async () => {
+ branches = createBranchesList(25);
+ stubRestApi('getRepoBranches').returns(Promise.resolve(branches));
+
+ element.params = {
+ view: GerritView.REPO,
+ repo: 'test' as RepoName,
+ detail: RepoDetailView.BRANCHES,
+ };
+
+ await element.paramsChanged();
+ await element.updateComplete;
+ });
+
+ test('items', () => {
+ assert.equal(queryAll<HTMLTableElement>(element, '.table').length, 25);
+ });
+ });
+
+ suite('filter', () => {
+ test('paramsChanged', async () => {
+ const stub = stubRestApi('getRepoBranches').returns(
+ Promise.resolve(branches)
+ );
+ element.params = {
+ view: GerritView.REPO,
+ repo: 'test' as RepoName,
+ detail: RepoDetailView.BRANCHES,
+ filter: 'test',
+ offset: 25,
+ };
+ await element.paramsChanged();
+ assert.equal(stub.lastCall.args[0], 'test');
+ assert.equal(stub.lastCall.args[1], 'test');
+ assert.equal(stub.lastCall.args[2], 25);
+ assert.equal(stub.lastCall.args[3], 25);
+ });
+ });
+
+ suite('404', () => {
+ test('fires page-error', async () => {
+ const response = {status: 404} as Response;
+ stubRestApi('getRepoBranches').callsFake(
+ (_filter, _repo, _reposBranchesPerPage, _opt_offset, errFn) => {
+ if (errFn !== undefined) {
+ errFn(response);
+ }
+ return Promise.resolve([]);
+ }
+ );
+
+ const promise = mockPromise();
+ addListenerForTest(document, 'page-error', e => {
+ assert.deepEqual((e as PageErrorEvent).detail.response, response);
+ promise.resolve();
+ });
+
+ element.params = {
+ view: GerritView.REPO,
+ repo: 'test' as RepoName,
+ detail: RepoDetailView.BRANCHES,
+ filter: 'test',
+ offset: 25,
+ };
+ element.paramsChanged();
+ await promise;
+ });
+ });
+ });
+
+ suite('Tags', () => {
+ let element: GrRepoDetailList;
+ let tags: TagInfo[];
+
+ setup(async () => {
+ element = basicFixture.instantiate();
+ await element.updateComplete;
+ element.detailType = RepoDetailView.TAGS;
+ sinon.stub(page, 'show');
+ });
+
+ suite('list of repo tags', () => {
+ setup(async () => {
+ tags = createTagsList(26);
+ stubRestApi('getRepoTags').returns(Promise.resolve(tags));
+
+ element.params = {
+ view: GerritView.REPO,
+ repo: 'test' as RepoName,
+ detail: RepoDetailView.TAGS,
+ };
+
+ await element.paramsChanged();
+ await element.updateComplete;
+ });
+
+ test('test for tag in the list', async () => {
+ assert.equal(element.items![2].ref, 'refs/tags/test2');
+ });
+
+ test('test for tag message in the list', async () => {
+ assert.equal((element.items as TagInfo[])![2].message, 'Annotated tag');
+ });
+
+ test('test for tagger in the tag list', async () => {
+ const tagger = {
+ name: 'Test User',
+ email: 'test.user@gmail.com' as EmailAddress,
+ date: '2017-09-19 14:54:00.000000000' as Timestamp,
+ tz: 540 as TimezoneOffset,
+ };
+
+ assert.deepEqual((element.items as TagInfo[])![2].tagger, tagger);
+ });
+
+ test('test for web links in the tags list', async () => {
+ assert.equal(
+ element.items![2].web_links![0].url,
+ 'https://git.example.org/tag/test;refs/tags/test2'
+ );
+ });
+
+ test('test for refs/tags/ being striped from ref', async () => {
+ assert.equal(
+ element.stripRefs(element.items![2].ref, element.detailType),
+ 'test2'
+ );
+ });
+
+ test('items', () => {
+ assert.equal(element.items!.slice(0, SHOWN_ITEMS_COUNT)!.length, 25);
+ });
+ });
+
+ suite('list with less then 25 tags', () => {
+ setup(async () => {
+ tags = createTagsList(25);
+ stubRestApi('getRepoTags').returns(Promise.resolve(tags));
+
+ element.params = {
+ view: GerritView.REPO,
+ repo: 'test' as RepoName,
+ detail: RepoDetailView.TAGS,
+ };
+
+ await element.paramsChanged();
+ await element.updateComplete;
+ });
+
+ test('items', () => {
+ assert.equal(element.items!.slice(0, SHOWN_ITEMS_COUNT)!.length, 25);
+ });
+ });
+
+ suite('filter', () => {
+ test('paramsChanged', async () => {
+ const stub = stubRestApi('getRepoTags').returns(Promise.resolve(tags));
+ element.params = {
+ view: GerritView.REPO,
+ repo: 'test' as RepoName,
+ detail: RepoDetailView.TAGS,
+ filter: 'test',
+ offset: 25,
+ };
+ await element.paramsChanged();
+ assert.equal(stub.lastCall.args[0], 'test');
+ assert.equal(stub.lastCall.args[1], 'test');
+ assert.equal(stub.lastCall.args[2], 25);
+ assert.equal(stub.lastCall.args[3], 25);
+ });
+ });
+
+ suite('create new', () => {
+ test('handleCreateClicked called when create-click fired', () => {
+ const handleCreateClickedStub = sinon.stub(
+ element,
+ 'handleCreateClicked'
+ );
+ queryAndAssert<GrListView>(element, 'gr-list-view').dispatchEvent(
+ new CustomEvent('create-clicked', {
+ composed: true,
+ bubbles: true,
+ })
+ );
+ assert.isTrue(handleCreateClickedStub.called);
+ });
+
+ test('handleCreateClicked opens modal', () => {
+ queryAndAssert<GrOverlay>(element, '#createOverlay');
+ const openStub = sinon.stub(
+ queryAndAssert<GrOverlay>(element, '#createOverlay'),
+ 'open'
+ );
+ element.handleCreateClicked();
+ assert.isTrue(openStub.called);
+ });
+
+ test('handleCreateItem called when confirm fired', () => {
+ const handleCreateItemStub = sinon.stub(element, 'handleCreateItem');
+ queryAndAssert<GrDialog>(element, '#createDialog').dispatchEvent(
+ new CustomEvent('confirm', {
+ composed: true,
+ bubbles: true,
+ })
+ );
+ assert.isTrue(handleCreateItemStub.called);
+ });
+
+ test('handleCloseCreate called when cancel fired', () => {
+ const handleCloseCreateStub = sinon.stub(element, 'handleCloseCreate');
+ queryAndAssert<GrDialog>(element, '#createDialog').dispatchEvent(
+ new CustomEvent('cancel', {
+ composed: true,
+ bubbles: true,
+ })
+ );
+ assert.isTrue(handleCloseCreateStub.called);
+ });
+ });
+
+ suite('404', () => {
+ test('fires page-error', async () => {
+ const response = {status: 404} as Response;
+ stubRestApi('getRepoTags').callsFake(
+ (_filter, _repo, _reposTagsPerPage, _opt_offset, errFn) => {
+ if (errFn !== undefined) {
+ errFn(response);
+ }
+ return Promise.resolve([]);
+ }
+ );
+
+ const promise = mockPromise();
+ addListenerForTest(document, 'page-error', e => {
+ assert.deepEqual((e as PageErrorEvent).detail.response, response);
+ promise.resolve();
+ });
+
+ element.params = {
+ view: GerritView.REPO,
+ repo: 'test' as RepoName,
+ detail: RepoDetailView.TAGS,
+ filter: 'test',
+ offset: 25,
+ };
+ element.paramsChanged();
+ await promise;
+ });
+ });
+
+ test('computeItemName', () => {
+ assert.equal(element.computeItemName(RepoDetailView.BRANCHES), 'Branch');
+ assert.equal(element.computeItemName(RepoDetailView.TAGS), 'Tag');
+ });
+ });
+});
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 adcfb64..48983d7 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
@@ -14,24 +14,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/gr-table-styles';
-import '../../../styles/shared-styles';
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 {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-repo-list_html';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {customElement, property, observe, computed} from '@polymer/decorators';
import {AppElementAdminParams} from '../../gr-app-types';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
-import {RepoName, ProjectInfoWithName} from '../../../types/common';
+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 {fireTitleChange} from '../../../utils/event-util';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {encodeURL, getBaseUrl} from '../../../utils/url-util';
+import {tableStyles} from '../../../styles/gr-table-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, css, html} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
declare global {
interface HTMLElementTagNameMap {
@@ -39,151 +42,262 @@
}
}
-export interface GrRepoList {
- $: {
- createOverlay: GrOverlay;
- createNewModal: GrCreateRepoDialog;
- };
-}
-
@customElement('gr-repo-list')
-export class GrRepoList extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
+export class GrRepoList extends LitElement {
+ readonly path = '/admin/repos';
+
+ @query('#createOverlay') private createOverlay?: GrOverlay;
+
+ @query('#createNewModal') private createNewModal?: GrCreateRepoDialog;
@property({type: Object})
params?: AppElementAdminParams;
- @property({type: Number})
- _offset = 0;
+ // private but used in test
+ @state() offset = 0;
- @property({type: String})
- readonly _path = '/admin/repos';
+ @state() private newRepoName = false;
- @property({type: Boolean})
- _hasNewRepoName = false;
+ @state() private createNewCapability = false;
- @property({type: Boolean})
- _createNewCapability = false;
+ // private but used in test
+ @state() repos: ProjectInfoWithName[] = [];
- @property({type: Array})
- _repos: ProjectInfoWithName[] = [];
+ // private but used in test
+ @state() reposPerPage = 25;
- @property({type: Number})
- _reposPerPage = 25;
+ // private but used in test
+ @state() loading = true;
- @property({type: Boolean})
- _loading = true;
+ // private but used in test
+ @state() filter = '';
- @property({type: String})
- _filter = '';
+ private readonly restApiService = getAppContext().restApiService;
- @computed('_repos')
- get _shownRepos() {
- return this._repos.slice(0, SHOWN_ITEMS_COUNT);
- }
-
- private readonly restApiService = appContext.restApiService;
-
- override connectedCallback() {
+ override async connectedCallback() {
super.connectedCallback();
- this._getCreateRepoCapability();
+ await this.getCreateRepoCapability();
fireTitleChange(this, 'Repos');
- this._maybeOpenCreateOverlay(this.params);
+ this.maybeOpenCreateOverlay(this.params);
}
- @observe('params')
- _paramsChanged(params: AppElementAdminParams) {
- this._loading = true;
- this._filter = params?.filter ?? '';
- this._offset = Number(params?.offset ?? 0);
+ static override get styles() {
+ return [
+ tableStyles,
+ sharedStyles,
+ css`
+ .genericList tr td:last-of-type {
+ text-align: left;
+ }
+ .genericList tr th:last-of-type {
+ text-align: left;
+ }
+ .readOnly {
+ text-align: center;
+ }
+ .changesLink,
+ .name,
+ .repositoryBrowser,
+ .readOnly {
+ white-space: nowrap;
+ }
+ `,
+ ];
+ }
- return this._getRepos(this._filter, this._reposPerPage, this._offset);
+ override render() {
+ return html`
+ <gr-list-view
+ .createNew=${this.createNewCapability}
+ .filter=${this.filter}
+ .itemsPerPage=${this.reposPerPage}
+ .items=${this.repos}
+ .loading=${this.loading}
+ .offset=${this.offset}
+ .path=${this.path}
+ @create-clicked=${() => this.handleCreateClicked()}
+ >
+ <table id="list" class="genericList">
+ <tbody>
+ <tr class="headerRow">
+ <th class="name topHeader">Repository Name</th>
+ <th class="repositoryBrowser topHeader">Repository Browser</th>
+ <th class="changesLink topHeader">Changes</th>
+ <th class="topHeader readOnly">Read only</th>
+ <th class="description topHeader">Repository Description</th>
+ </tr>
+ <tr
+ id="loading"
+ class="loadingMsg ${this.computeLoadingClass(this.loading)}"
+ >
+ <td>Loading...</td>
+ </tr>
+ </tbody>
+ <tbody class="${this.computeLoadingClass(this.loading)}">
+ ${this.renderRepoList()}
+ </tbody>
+ </table>
+ </gr-list-view>
+ <gr-overlay id="createOverlay" with-backdrop>
+ <gr-dialog
+ id="createDialog"
+ class="confirmDialog"
+ ?disabled=${!this.newRepoName}
+ confirm-label="Create"
+ @confirm=${() => this.handleCreateRepo()}
+ @cancel=${() => this.handleCloseCreate()}
+ >
+ <div class="header" slot="header">Create Repository</div>
+ <div class="main" slot="main">
+ <gr-create-repo-dialog
+ id="createNewModal"
+ @new-repo-name=${() => this.handleNewRepoName()}
+ ></gr-create-repo-dialog>
+ </div>
+ </gr-dialog>
+ </gr-overlay>
+ `;
+ }
+
+ private renderRepoList() {
+ const shownRepos = this.repos.slice(0, SHOWN_ITEMS_COUNT);
+ return shownRepos.map(item => this.renderRepo(item));
+ }
+
+ private renderRepo(item: ProjectInfoWithName) {
+ return html`
+ <tr class="table">
+ <td class="name">
+ <a href="${this.computeRepoUrl(item.name)}">${item.name}</a>
+ </td>
+ <td class="repositoryBrowser">${this.renderWebLinks(item)}</td>
+ <td class="changesLink">
+ <a href="${this.computeChangesLink(item.name)}">view all</a>
+ </td>
+ <td class="readOnly">
+ ${item.state === ProjectState.READ_ONLY ? 'Y' : ''}
+ </td>
+ <td class="description">${item.description}</td>
+ </tr>
+ `;
+ }
+
+ private renderWebLinks(links: ProjectInfoWithName) {
+ const webLinks = links.web_links ? links.web_links : [];
+ return webLinks.map(link => this.renderWebLink(link));
+ }
+
+ private renderWebLink(link: WebLinkInfo) {
+ return html`
+ <a href="${link.url}" class="webLink" rel="noopener" target="_blank">
+ ${link.name}
+ </a>
+ `;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('params')) {
+ this._paramsChanged();
+ }
+ }
+
+ async _paramsChanged() {
+ const params = this.params;
+ this.loading = true;
+ this.filter = params?.filter ?? '';
+ this.offset = Number(params?.offset ?? 0);
+
+ return await this.getRepos();
}
/**
- * Opens the create overlay if the route has a hash 'create'
+ * Opens the create overlay if the route has a hash 'create'.
+ *
+ * private but used in test
*/
- _maybeOpenCreateOverlay(params?: AppElementAdminParams) {
+ maybeOpenCreateOverlay(params?: AppElementAdminParams) {
if (params?.openCreateModal) {
- this.$.createOverlay.open();
+ this.createOverlay?.open();
}
}
- _computeRepoUrl(name: string) {
- return getBaseUrl() + this._path + '/' + encodeURL(name, true);
+ private computeRepoUrl(name: string) {
+ return `${getBaseUrl()}${this.path}/${encodeURL(name, true)}`;
}
- _computeChangesLink(name: string) {
+ private computeChangesLink(name: string) {
return GerritNav.getUrlForProjectChanges(name as RepoName);
}
- _getCreateRepoCapability() {
- return this.restApiService.getAccount().then(account => {
- if (!account) {
- return;
- }
- return this.restApiService
- .getAccountCapabilities(['createProject'])
- .then(capabilities => {
- if (capabilities?.createProject) {
- this._createNewCapability = true;
- }
- });
- });
- }
+ private async getCreateRepoCapability() {
+ const account = await this.restApiService.getAccount();
- _getRepos(filter: string, reposPerPage: number, offset?: number) {
- this._repos = [];
- return this.restApiService
- .getRepos(filter, reposPerPage, offset)
- .then(repos => {
- // Late response.
- if (filter !== this._filter || !repos) {
- return;
- }
- this._repos = repos.filter(repo =>
- repo.name.toLowerCase().includes(filter.toLowerCase())
- );
- this._loading = false;
- });
- }
+ if (!account) return;
- _refreshReposList() {
- this.restApiService.invalidateReposCache();
- return this._getRepos(this._filter, this._reposPerPage, this._offset);
- }
-
- _handleCreateRepo() {
- this.$.createNewModal.handleCreateRepo().then(() => {
- this._refreshReposList();
- });
- }
-
- _handleCloseCreate() {
- this.$.createOverlay.close();
- }
-
- _handleCreateClicked() {
- this.$.createOverlay.open().then(() => {
- this.$.createNewModal.focus();
- });
- }
-
- _readOnly(repo: ProjectInfoWithName) {
- return repo.state === ProjectState.READ_ONLY ? 'Y' : '';
- }
-
- _computeWeblink(repo: ProjectInfoWithName) {
- if (!repo.web_links) {
- return '';
+ const accountCapabilities =
+ await this.restApiService.getAccountCapabilities(['createProject']);
+ if (accountCapabilities?.createProject) {
+ this.createNewCapability = true;
}
- const webLinks = repo.web_links;
- return webLinks.length ? webLinks : null;
+
+ return account;
}
+ // private but used in test
+ async getRepos() {
+ this.repos = [];
+
+ // We save the filter before getting the repos
+ // and then we check the value hasn't changed aftwards.
+ const filter = this.filter;
+
+ const repos = await this.restApiService.getRepos(
+ this.filter,
+ this.reposPerPage,
+ this.offset
+ );
+
+ // Late response.
+ if (filter !== this.filter || !repos) return;
+
+ this.repos = repos.filter(repo =>
+ repo.name.toLowerCase().includes(filter.toLowerCase())
+ );
+ this.loading = false;
+
+ return repos;
+ }
+
+ private async refreshReposList() {
+ this.restApiService.invalidateReposCache();
+ return await this.getRepos();
+ }
+
+ // private but used in test
+ async handleCreateRepo() {
+ await this.createNewModal?.handleCreateRepo();
+ await this.refreshReposList();
+ }
+
+ // private but used in test
+ handleCloseCreate() {
+ this.createOverlay?.close();
+ }
+
+ // private but used in test
+ handleCreateClicked() {
+ this.createOverlay?.open().then(() => {
+ this.createNewModal?.focus();
+ });
+ }
+
+ // private but used in test
computeLoadingClass(loading: boolean) {
return loading ? 'loading' : '';
}
+
+ private handleNewRepoName() {
+ if (!this.createNewModal) return;
+ this.newRepoName = this.createNewModal.nameChanged;
+ }
}
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.ts b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.ts
deleted file mode 100644
index e1a7f489..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-table-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style>
- .genericList tr td:last-of-type {
- text-align: left;
- }
- .genericList tr th:last-of-type {
- text-align: left;
- }
- .readOnly {
- text-align: center;
- }
- .changesLink,
- .name,
- .repositoryBrowser,
- .readOnly {
- white-space: nowrap;
- }
- </style>
- <gr-list-view
- create-new="[[_createNewCapability]]"
- filter="[[_filter]]"
- items-per-page="[[_reposPerPage]]"
- items="[[_repos]]"
- loading="[[_loading]]"
- offset="[[_offset]]"
- on-create-clicked="_handleCreateClicked"
- path="[[_path]]"
- >
- <table id="list" class="genericList">
- <tbody>
- <tr class="headerRow">
- <th class="name topHeader">Repository Name</th>
- <th class="repositoryBrowser topHeader">Repository Browser</th>
- <th class="changesLink topHeader">Changes</th>
- <th class="topHeader readOnly">Read only</th>
- <th class="description topHeader">Repository Description</th>
- </tr>
- <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
- <td>Loading...</td>
- </tr>
- </tbody>
- <tbody class$="[[computeLoadingClass(_loading)]]">
- <template is="dom-repeat" items="[[_shownRepos]]">
- <tr class="table">
- <td class="name">
- <a href$="[[_computeRepoUrl(item.name)]]">[[item.name]]</a>
- </td>
- <td class="repositoryBrowser">
- <template
- is="dom-repeat"
- items="[[_computeWeblink(item)]]"
- as="link"
- >
- <a
- href$="[[link.url]]"
- class="webLink"
- rel="noopener"
- target="_blank"
- >
- [[link.name]]
- </a>
- </template>
- </td>
- <td class="changesLink">
- <a href$="[[_computeChangesLink(item.name)]]">view all</a>
- </td>
- <td class="readOnly">[[_readOnly(item)]]</td>
- <td class="description">[[item.description]]</td>
- </tr>
- </template>
- </tbody>
- </table>
- </gr-list-view>
- <gr-overlay id="createOverlay" with-backdrop="">
- <gr-dialog
- id="createDialog"
- class="confirmDialog"
- disabled="[[!_hasNewRepoName]]"
- confirm-label="Create"
- on-confirm="_handleCreateRepo"
- on-cancel="_handleCloseCreate"
- >
- <div class="header" slot="header">Create Repository</div>
- <div class="main" slot="main">
- <gr-create-repo-dialog
- has-new-repo-name="{{_hasNewRepoName}}"
- id="createNewModal"
- ></gr-create-repo-dialog>
- </div>
- </gr-dialog>
- </gr-overlay>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js
deleted file mode 100644
index 8fef4d0..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js
+++ /dev/null
@@ -1,189 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-repo-list.js';
-import {page} from '../../../utils/page-wrapper-utils.js';
-import 'lodash/lodash.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-repo-list');
-
-function createRepo(name, counter) {
- return {
- id: `${name}${counter}`,
- name: `${name}`,
- state: 'ACTIVE',
- web_links: [
- {
- name: 'diffusion',
- url: `https://phabricator.example.org/r/project/${name}${counter}`,
- },
- ],
- };
-}
-
-let counter;
-const repoGenerator = () => createRepo('test', ++counter);
-
-suite('gr-repo-list tests', () => {
- let element;
- let repos;
-
- let value;
-
- setup(() => {
- sinon.stub(page, 'show');
- element = basicFixture.instantiate();
- counter = 0;
- });
-
- suite('list with repos', () => {
- setup(async () => {
- repos = _.times(26, repoGenerator);
- stubRestApi('getRepos').returns(Promise.resolve(repos));
- await element._paramsChanged(value);
- await flush();
- });
-
- test('test for test repo in the list', async () => {
- await flush();
- assert.equal(element._repos[1].id, 'test2');
- });
-
- test('_shownRepos', () => {
- assert.equal(element._shownRepos.length, 25);
- });
-
- test('_maybeOpenCreateOverlay', () => {
- const overlayOpen = sinon.stub(element.$.createOverlay, 'open');
- element._maybeOpenCreateOverlay();
- assert.isFalse(overlayOpen.called);
- const params = {};
- element._maybeOpenCreateOverlay(params);
- assert.isFalse(overlayOpen.called);
- params.openCreateModal = true;
- element._maybeOpenCreateOverlay(params);
- assert.isTrue(overlayOpen.called);
- });
- });
-
- suite('list with less then 25 repos', () => {
- setup(async () => {
- repos = _.times(25, repoGenerator);
- stubRestApi('getRepos').returns(Promise.resolve(repos));
- await element._paramsChanged(value);
- await flush();
- });
-
- test('_shownRepos', () => {
- assert.equal(element._shownRepos.length, 25);
- });
- });
-
- suite('filter', () => {
- let reposFiltered;
- setup(() => {
- repos = _.times(25, repoGenerator);
- reposFiltered = _.times(1, repoGenerator);
- });
-
- test('_paramsChanged', async () => {
- const repoStub = stubRestApi('getRepos');
- repoStub.returns(Promise.resolve(repos));
- const value = {
- filter: 'test',
- offset: 25,
- };
- await element._paramsChanged(value);
- assert.isTrue(repoStub.lastCall.calledWithExactly('test', 25, 25));
- });
-
- test('latest repos requested are always set', async () => {
- const repoStub = stubRestApi('getRepos');
- repoStub.withArgs('test').returns(Promise.resolve(repos));
- repoStub.withArgs('filter').returns(Promise.resolve(reposFiltered));
- element._filter = 'test';
-
- // Repos are not set because the element._filter differs.
- await element._getRepos('filter', 25, 0);
- assert.deepEqual(element._repos, []);
- });
-
- test('filter is case insensitive', async () => {
- const repoStub = stubRestApi('getRepos');
- const repos = [createRepo('aSDf', 0)];
- repoStub.withArgs('asdf').returns(Promise.resolve(repos));
- element._filter = 'asdf';
- await element._getRepos('asdf', 25, 0);
- assert.equal(element._repos.length, 1);
- });
- });
-
- suite('loading', () => {
- test('correct contents are displayed', () => {
- assert.isTrue(element._loading);
- assert.equal(element.computeLoadingClass(element._loading), 'loading');
- assert.equal(getComputedStyle(element.$.loading).display, 'block');
-
- element._loading = false;
- element._repos = _.times(25, repoGenerator);
-
- flush();
- assert.equal(element.computeLoadingClass(element._loading), '');
- assert.equal(getComputedStyle(element.$.loading).display, 'none');
- });
- });
-
- suite('create new', () => {
- test('_handleCreateClicked called when create-click fired', () => {
- sinon.stub(element, '_handleCreateClicked');
- element.shadowRoot
- .querySelector('gr-list-view').dispatchEvent(
- new CustomEvent('create-clicked', {
- composed: true, bubbles: true,
- }));
- assert.isTrue(element._handleCreateClicked.called);
- });
-
- test('_handleCreateClicked opens modal', () => {
- const openStub = sinon.stub(element.$.createOverlay, 'open').returns(
- Promise.resolve());
- element._handleCreateClicked();
- assert.isTrue(openStub.called);
- });
-
- test('_handleCreateRepo called when confirm fired', () => {
- sinon.stub(element, '_handleCreateRepo');
- element.$.createDialog.dispatchEvent(
- new CustomEvent('confirm', {
- composed: true, bubbles: true,
- }));
- assert.isTrue(element._handleCreateRepo.called);
- });
-
- test('_handleCloseCreate called when cancel fired', () => {
- sinon.stub(element, '_handleCloseCreate');
- element.$.createDialog.dispatchEvent(
- new CustomEvent('cancel', {
- composed: true, bubbles: true,
- }));
- assert.isTrue(element._handleCloseCreate.called);
- });
- });
-});
-
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
new file mode 100644
index 0000000..25052a3
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.ts
@@ -0,0 +1,246 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-repo-list';
+import {GrRepoList} from './gr-repo-list';
+import {page} from '../../../utils/page-wrapper-utils';
+import {
+ mockPromise,
+ queryAndAssert,
+ stubRestApi,
+} from '../../../test/test-utils';
+import {
+ UrlEncodedRepoName,
+ ProjectInfoWithName,
+ RepoName,
+} from '../../../types/common';
+import {AppElementAdminParams} from '../../gr-app-types';
+import {ProjectState, 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';
+
+const basicFixture = fixtureFromElement('gr-repo-list');
+
+function createRepo(name: string, counter: number) {
+ return {
+ id: `${name}${counter}` as UrlEncodedRepoName,
+ name: `${name}` as RepoName,
+ state: 'ACTIVE' as ProjectState,
+ web_links: [
+ {
+ name: 'diffusion',
+ url: `https://phabricator.example.org/r/project/${name}${counter}`,
+ },
+ ],
+ };
+}
+
+function createRepoList(name: string, n: number) {
+ const repos = [];
+ for (let i = 0; i < n; ++i) {
+ repos.push(createRepo(name, i));
+ }
+ return repos;
+}
+
+suite('gr-repo-list tests', () => {
+ let element: GrRepoList;
+ let repos: ProjectInfoWithName[];
+
+ setup(async () => {
+ sinon.stub(page, 'show');
+ element = basicFixture.instantiate();
+ await element.updateComplete;
+ });
+
+ suite('list with repos', () => {
+ setup(async () => {
+ repos = createRepoList('test', 26);
+ stubRestApi('getRepos').returns(Promise.resolve(repos));
+ await element._paramsChanged();
+ await element.updateComplete;
+ });
+
+ test('test for test repo in the list', async () => {
+ await element.updateComplete;
+ assert.equal(element.repos[0].id, 'test0');
+ assert.equal(element.repos[1].id, 'test1');
+ assert.equal(element.repos[2].id, 'test2');
+ });
+
+ test('shownRepos', () => {
+ assert.equal(element.repos.slice(0, SHOWN_ITEMS_COUNT).length, 25);
+ });
+
+ test('maybeOpenCreateOverlay', () => {
+ const overlayOpen = sinon.stub(
+ queryAndAssert<GrOverlay>(element, '#createOverlay'),
+ 'open'
+ );
+ element.maybeOpenCreateOverlay();
+ assert.isFalse(overlayOpen.called);
+ element.maybeOpenCreateOverlay(undefined);
+ assert.isFalse(overlayOpen.called);
+ const params: AppElementAdminParams = {
+ view: GerritView.ADMIN,
+ adminView: '',
+ openCreateModal: true,
+ };
+ element.maybeOpenCreateOverlay(params);
+ assert.isTrue(overlayOpen.called);
+ });
+ });
+
+ suite('list with less then 25 repos', () => {
+ setup(async () => {
+ repos = createRepoList('test', 25);
+ stubRestApi('getRepos').returns(Promise.resolve(repos));
+ await element._paramsChanged();
+ await element.updateComplete;
+ });
+
+ test('shownRepos', () => {
+ assert.equal(element.repos.slice(0, SHOWN_ITEMS_COUNT).length, 25);
+ });
+ });
+
+ suite('filter', () => {
+ let reposFiltered: ProjectInfoWithName[];
+
+ setup(() => {
+ repos = createRepoList('test', 25);
+ reposFiltered = createRepoList('filter', 1);
+ });
+
+ test('_paramsChanged', async () => {
+ const repoStub = stubRestApi('getRepos');
+ repoStub.returns(Promise.resolve(repos));
+ element.params = {
+ view: GerritView.ADMIN,
+ adminView: '',
+ filter: 'test',
+ offset: 25,
+ } as AppElementAdminParams;
+ await element._paramsChanged();
+ assert.isTrue(repoStub.lastCall.calledWithExactly('test', 25, 25));
+ });
+
+ test('latest repos requested are always set', async () => {
+ const repoStub = stubRestApi('getRepos');
+ const promise = mockPromise<ProjectInfoWithName[]>();
+ repoStub.withArgs('filter', 25).returns(promise);
+
+ element.filter = 'test';
+ element.reposPerPage = 25;
+ element.offset = 0;
+
+ // Repos are not set because the element.filter differs.
+ const p = element.getRepos();
+ element.filter = 'filter';
+ promise.resolve(reposFiltered);
+ await p;
+ assert.deepEqual(element.repos, []);
+ });
+
+ test('filter is case insensitive', async () => {
+ const repoStub = stubRestApi('getRepos');
+ const repos = [createRepo('aSDf', 0)];
+ repoStub.withArgs('asdf', 25).returns(Promise.resolve(repos));
+
+ element.filter = 'asdf';
+ element.reposPerPage = 25;
+ element.offset = 0;
+
+ await element.getRepos();
+ assert.equal(element.repos.length, 1);
+ });
+ });
+
+ suite('loading', () => {
+ test('correct contents are displayed', async () => {
+ assert.isTrue(element.loading);
+ assert.equal(element.computeLoadingClass(element.loading), 'loading');
+ assert.equal(
+ getComputedStyle(
+ queryAndAssert<HTMLTableRowElement>(element, '#loading')
+ ).display,
+ 'block'
+ );
+
+ element.loading = false;
+ element.repos = createRepoList('test', 25);
+
+ await element.updateComplete;
+ assert.equal(element.computeLoadingClass(element.loading), '');
+ assert.equal(
+ getComputedStyle(
+ queryAndAssert<HTMLTableRowElement>(element, '#loading')
+ ).display,
+ 'none'
+ );
+ });
+ });
+
+ suite('create new', () => {
+ test('handleCreateClicked called when create-clicked fired', () => {
+ const handleCreateClickedStub = sinon.stub(
+ element,
+ 'handleCreateClicked'
+ );
+ queryAndAssert<GrListView>(element, 'gr-list-view').dispatchEvent(
+ new CustomEvent('create-clicked', {
+ composed: true,
+ bubbles: true,
+ })
+ );
+ assert.isTrue(handleCreateClickedStub.called);
+ });
+
+ test('handleCreateClicked opens modal', () => {
+ const openStub = sinon
+ .stub(queryAndAssert<GrOverlay>(element, '#createOverlay'), 'open')
+ .returns(Promise.resolve());
+ element.handleCreateClicked();
+ assert.isTrue(openStub.called);
+ });
+
+ test('handleCreateRepo called when confirm fired', () => {
+ const handleCreateRepoStub = sinon.stub(element, 'handleCreateRepo');
+ queryAndAssert<GrDialog>(element, '#createDialog').dispatchEvent(
+ new CustomEvent('confirm', {
+ composed: true,
+ bubbles: false,
+ })
+ );
+ assert.isTrue(handleCreateRepoStub.called);
+ });
+
+ test('handleCloseCreate called when cancel fired', () => {
+ const handleCloseCreateStub = sinon.stub(element, 'handleCloseCreate');
+ queryAndAssert<GrDialog>(element, '#createDialog').dispatchEvent(
+ new CustomEvent('cancel', {
+ composed: true,
+ bubbles: false,
+ })
+ );
+ assert.isTrue(handleCloseCreateStub.called);
+ });
+ });
+});
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
index 9b2bf68..f9f760c 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
@@ -36,13 +36,13 @@
PluginConfigOptionsChangedEventDetail,
PluginOption,
} from './gr-repo-plugin-config-types';
+import {paperStyles} from '../../../styles/gr-paper-styles';
const PLUGIN_CONFIG_CHANGED_EVENT_NAME = 'plugin-config-changed';
export interface ConfigChangeInfo {
_key: string; // parameterName of PluginParameterToConfigParameterInfoMap
info: ConfigParameterInfo;
- notifyPath: string;
}
export interface PluginData {
@@ -53,7 +53,6 @@
export interface PluginConfigChangeDetail {
name: string; // parameterName of PluginParameterToConfigParameterInfoMap
config: PluginParameterToConfigParameterInfoMap;
- notifyPath: string;
}
@customElement('gr-repo-plugin-config')
@@ -74,6 +73,7 @@
return [
sharedStyles,
formStyles,
+ paperStyles,
subpageStyles,
css`
.inherited {
@@ -148,6 +148,7 @@
<gr-plugin-config-array-editor
@plugin-config-option-changed=${this._handleArrayChange}
.pluginOption="${option}"
+ ?disabled=${this.disabled || !option.info.editable}
></gr-plugin-config-array-editor>
`;
} else if (option.info.type === ConfigParameterInfoType.BOOLEAN) {
@@ -249,7 +250,6 @@
return {
_key,
info,
- notifyPath: `${_key}.value`,
};
}
@@ -257,7 +257,7 @@
this._handleChange(e.detail);
}
- _handleChange({_key, info, notifyPath}: ConfigChangeInfo) {
+ _handleChange({_key, info}: ConfigChangeInfo) {
// If pluginData is not set, editors are not created and this method
// can't be called
const {name, config} = this.pluginData!;
@@ -266,7 +266,6 @@
const detail: PluginConfigChangeDetail = {
name,
config: {...config, [_key]: info},
- notifyPath: `${name}.${notifyPath}`,
};
this.dispatchEvent(
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.js b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.js
deleted file mode 100644
index 8c2e6b3..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.js
+++ /dev/null
@@ -1,157 +0,0 @@
-/**
- * @license
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-repo-plugin-config.js';
-
-const basicFixture = fixtureFromElement('gr-repo-plugin-config');
-
-suite('gr-repo-plugin-config tests', () => {
- let element;
-
- setup(async () => {
- element = basicFixture.instantiate();
- await flush();
- });
-
- test('_computePluginConfigOptions', () => {
- assert.deepEqual(element._computePluginConfigOptions({config: {}}), []);
- assert.deepEqual(element._computePluginConfigOptions(
- {config: {testKey: 'testInfo'}}),
- [{_key: 'testKey', info: 'testInfo'}]);
- });
-
- test('_handleChange', () => {
- const eventStub = sinon.stub(element, 'dispatchEvent');
- element.pluginData = {
- name: 'testName',
- config: {plugin: {value: 'test'}},
- };
- element._handleChange({
- _key: 'plugin',
- info: {value: 'newTest'},
- notifyPath: 'plugin.value',
- });
-
- assert.isTrue(eventStub.called);
-
- const {detail} = eventStub.lastCall.args[0];
- assert.equal(detail.name, 'testName');
- assert.deepEqual(detail.config, {plugin: {value: 'newTest'}});
- assert.equal(detail.notifyPath, 'testName.plugin.value');
- });
-
- suite('option types', () => {
- let changeStub;
- let buildStub;
-
- setup(() => {
- changeStub = sinon.stub(element, '_handleChange');
- buildStub = sinon.stub(element, '_buildConfigChangeInfo');
- });
-
- test('ARRAY type option', async () => {
- element.pluginData = {
- name: 'testName',
- config: {plugin: {value: 'test', type: 'ARRAY', editable: true}},
- };
- await flush();
-
- const editor = element.shadowRoot
- .querySelector('gr-plugin-config-array-editor');
- assert.ok(editor);
- element._handleArrayChange({detail: 'test'});
- assert.isTrue(changeStub.called);
- assert.equal(changeStub.lastCall.args[0], 'test');
- });
-
- test('BOOLEAN type option', async () => {
- element.pluginData = {
- name: 'testName',
- config: {plugin: {value: 'true', type: 'BOOLEAN', editable: true}},
- };
- await flush();
-
- const toggle = element.shadowRoot
- .querySelector('paper-toggle-button');
- assert.ok(toggle);
- toggle.click();
- await flush();
-
- assert.isTrue(buildStub.called);
- assert.deepEqual(buildStub.lastCall.args, ['false', 'plugin']);
-
- assert.isTrue(changeStub.called);
- });
-
- test('INT/LONG/STRING type option', async () => {
- element.pluginData = {
- name: 'testName',
- config: {plugin: {value: 'test', type: 'STRING', editable: true}},
- };
- await flush();
-
- const input = element.shadowRoot
- .querySelector('input');
- assert.ok(input);
- input.value = 'newTest';
- input.dispatchEvent(new Event('input'));
- await flush();
-
- assert.isTrue(buildStub.called);
- assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']);
-
- assert.isTrue(changeStub.called);
- });
-
- test('LIST type option', async () => {
- const permitted_values = ['test', 'newTest'];
- element.pluginData = {
- name: 'testName',
- config: {plugin:
- {value: 'test', type: 'LIST', editable: true, permitted_values},
- },
- };
- await flush();
-
- const select = element.shadowRoot
- .querySelector('select');
- assert.ok(select);
- select.value = 'newTest';
- select.dispatchEvent(new Event(
- 'change', {bubbles: true, composed: true}));
- await flush();
-
- assert.isTrue(buildStub.called);
- assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']);
-
- assert.isTrue(changeStub.called);
- });
- });
-
- test('_buildConfigChangeInfo', () => {
- element.pluginData = {
- name: 'testName',
- config: {plugin: {value: 'test'}},
- };
- const detail = element._buildConfigChangeInfo('newTest', 'plugin');
- assert.equal(detail._key, 'plugin');
- assert.deepEqual(detail.info, {value: 'newTest'});
- assert.equal(detail.notifyPath, 'plugin.value');
- });
-});
-
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.ts
new file mode 100644
index 0000000..fa3a635
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.ts
@@ -0,0 +1,207 @@
+/**
+ * @license
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-repo-plugin-config';
+import {GrRepoPluginConfig} from './gr-repo-plugin-config';
+import {PluginParameterToConfigParameterInfoMap} from '../../../types/common';
+import {ConfigParameterInfoType} from '../../../constants/constants';
+import {queryAndAssert} from '../../../test/test-utils';
+import {GrPluginConfigArrayEditor} from '../gr-plugin-config-array-editor/gr-plugin-config-array-editor';
+import {PaperToggleButtonElement} from '@polymer/paper-toggle-button/paper-toggle-button';
+
+const basicFixture = fixtureFromElement('gr-repo-plugin-config');
+
+suite('gr-repo-plugin-config tests', () => {
+ let element: GrRepoPluginConfig;
+
+ setup(async () => {
+ element = basicFixture.instantiate();
+ await element.updateComplete;
+ });
+
+ test('_computePluginConfigOptions', () => {
+ assert.deepEqual(
+ element._computePluginConfigOptions({
+ name: 'testInfo',
+ config: {
+ testKey: {display_name: 'testInfo plugin', type: 'STRING'},
+ } as PluginParameterToConfigParameterInfoMap,
+ }),
+ [
+ {
+ _key: 'testKey',
+ info: {
+ display_name: 'testInfo plugin',
+ type: 'STRING' as ConfigParameterInfoType,
+ },
+ },
+ ]
+ );
+ });
+
+ test('_handleChange', () => {
+ const eventStub = sinon.stub(element, 'dispatchEvent');
+ element.pluginData = {
+ name: 'testName',
+ config: {
+ plugin: {type: 'STRING' as ConfigParameterInfoType, value: 'test'},
+ },
+ };
+ element._handleChange({
+ _key: 'plugin',
+ info: {type: 'STRING' as ConfigParameterInfoType, value: 'newTest'},
+ });
+
+ assert.isTrue(eventStub.called);
+
+ const {detail} = eventStub.lastCall.args[0] as CustomEvent;
+ assert.equal(detail.name, 'testName');
+ assert.deepEqual(detail.config, {
+ plugin: {type: 'STRING' as ConfigParameterInfoType, value: 'newTest'},
+ });
+ });
+
+ suite('option types', () => {
+ let changeStub: sinon.SinonStub;
+ let buildStub: sinon.SinonStub;
+
+ setup(() => {
+ changeStub = sinon.stub(element, '_handleChange');
+ buildStub = sinon.stub(element, '_buildConfigChangeInfo');
+ });
+
+ test('ARRAY type option', async () => {
+ element.pluginData = {
+ name: 'testName',
+ config: {
+ plugin: {
+ value: 'test',
+ type: 'ARRAY' as ConfigParameterInfoType,
+ editable: true,
+ },
+ },
+ };
+ await element.updateComplete;
+
+ const editor = queryAndAssert<GrPluginConfigArrayEditor>(
+ element,
+ 'gr-plugin-config-array-editor'
+ );
+ assert.ok(editor);
+ element._handleArrayChange({detail: 'test'} as CustomEvent);
+ assert.isTrue(changeStub.called);
+ assert.equal(changeStub.lastCall.args[0], 'test');
+ });
+
+ test('BOOLEAN type option', async () => {
+ element.pluginData = {
+ name: 'testName',
+ config: {
+ plugin: {
+ value: 'true',
+ type: 'BOOLEAN' as ConfigParameterInfoType,
+ editable: true,
+ },
+ },
+ };
+ await element.updateComplete;
+
+ const toggle = queryAndAssert<PaperToggleButtonElement>(
+ element,
+ 'paper-toggle-button'
+ );
+ assert.ok(toggle);
+ toggle.click();
+ await element.updateComplete;
+
+ assert.isTrue(buildStub.called);
+ assert.deepEqual(buildStub.lastCall.args, ['false', 'plugin']);
+
+ assert.isTrue(changeStub.called);
+ });
+
+ test('INT/LONG/STRING type option', async () => {
+ element.pluginData = {
+ name: 'testName',
+ config: {
+ plugin: {
+ value: 'test',
+ type: 'STRING' as ConfigParameterInfoType,
+ editable: true,
+ },
+ },
+ };
+ await element.updateComplete;
+
+ const input = queryAndAssert<HTMLInputElement>(element, 'input');
+ assert.ok(input);
+ input.value = 'newTest';
+ input.dispatchEvent(new Event('input'));
+ await element.updateComplete;
+
+ assert.isTrue(buildStub.called);
+ assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']);
+
+ assert.isTrue(changeStub.called);
+ });
+
+ test('LIST type option', async () => {
+ const permitted_values = ['test', 'newTest'];
+ element.pluginData = {
+ name: 'testName',
+ config: {
+ plugin: {
+ value: 'test',
+ type: 'LIST' as ConfigParameterInfoType,
+ editable: true,
+ permitted_values,
+ },
+ },
+ };
+ await element.updateComplete;
+
+ const select = queryAndAssert<HTMLSelectElement>(element, 'select');
+ assert.ok(select);
+ select.value = 'newTest';
+ select.dispatchEvent(
+ new Event('change', {bubbles: true, composed: true})
+ );
+ await element.updateComplete;
+
+ assert.isTrue(buildStub.called);
+ assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']);
+
+ assert.isTrue(changeStub.called);
+ });
+ });
+
+ test('_buildConfigChangeInfo', () => {
+ element.pluginData = {
+ name: 'testName',
+ config: {
+ plugin: {type: 'STRING' as ConfigParameterInfoType, value: 'test'},
+ },
+ };
+ const detail = element._buildConfigChangeInfo('newTest', 'plugin');
+ assert.equal(detail._key, 'plugin');
+ assert.deepEqual(detail.info, {
+ type: 'STRING' as ConfigParameterInfoType,
+ value: 'newTest',
+ });
+ });
+});
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 cd5b095..5fc7bc8 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
@@ -14,38 +14,43 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
import '@polymer/iron-input/iron-input';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
+import '../../shared/gr-button/gr-button';
import '../../shared/gr-download-commands/gr-download-commands';
import '../../shared/gr-select/gr-select';
-import '../../../styles/gr-font-styles';
-import '../../../styles/gr-form-styles';
-import '../../../styles/gr-subpage-styles';
-import '../../../styles/shared-styles';
+import '../../shared/gr-textarea/gr-textarea';
import '../gr-repo-plugin-config/gr-repo-plugin-config';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-repo_html';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {customElement, property, observe} from '@polymer/decorators';
import {
ConfigInfo,
RepoName,
InheritedBooleanInfo,
SchemesInfoMap,
ConfigInput,
+ MaxObjectSizeLimitInfo,
PluginParameterToConfigParameterInfoMap,
- PluginNameToPluginParametersMap,
} from '../../../types/common';
-import {PluginData} from '../gr-repo-plugin-config/gr-repo-plugin-config';
-import {ProjectState} from '../../../constants/constants';
-import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
+import {
+ InheritedBooleanInfoConfiguredValue,
+ ProjectState,
+ SubmitType,
+} from '../../../constants/constants';
import {hasOwnProperty} from '../../../utils/common-util';
import {firePageError, fireTitleChange} from '../../../utils/event-util';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {WebLinkInfo} from '../../../types/diff';
import {ErrorCallback} from '../../../api/rest';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {subpageStyles} from '../../../styles/gr-subpage-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {BindValueChangeEvent} from '../../../types/events';
+import {deepClone} from '../../../utils/deep-util';
+import {LitElement, PropertyValues, css, html} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+import {subscribe} from '../../lit/subscription-controller';
const STATES = {
active: {value: ProjectState.ACTIVE, label: 'Active'},
@@ -81,92 +86,684 @@
},
};
-@customElement('gr-repo')
-export class GrRepo extends PolymerElement {
- static get template() {
- return htmlTemplate;
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-repo': GrRepo;
}
+}
+
+@customElement('gr-repo')
+export class GrRepo extends LitElement {
+ private schemes: string[] = [];
@property({type: String})
repo?: RepoName;
- @property({type: Boolean})
- _configChanged = false;
+ // private but used in test
+ @state() loading = true;
- @property({type: Boolean})
- _loading = true;
+ // private but used in test
+ @state() repoConfig?: ConfigInfo;
- @property({type: Boolean, observer: '_loggedInChanged'})
- _loggedIn = false;
+ // private but used in test
+ @state() readOnly = true;
- @property({type: Object})
- _repoConfig?: ConfigInfo;
+ @state() private states = Object.values(STATES);
- @property({
- type: Array,
- computed: '_computePluginData(_repoConfig.plugin_config.*)',
- })
- _pluginData?: PluginData[];
+ @state() private originalConfig?: ConfigInfo;
- @property({type: Boolean})
- _readOnly = true;
+ @state() private selectedScheme?: string;
- @property({type: Array})
- _states = Object.values(STATES);
+ // private but used in test
+ @state() schemesObj?: SchemesInfoMap;
- @property({
- type: Array,
- computed: '_computeSchemes(_schemesDefault, _schemesObj)',
- observer: '_schemesChanged',
- })
- _schemes: string[] = [];
+ @state() private weblinks: WebLinkInfo[] = [];
- // This is workaround to have _schemes with default value [],
- // because assignment doesn't work when property has a computed attribute.
- @property({type: Array})
- _schemesDefault: string[] = [];
+ @state() private pluginConfigChanged = false;
- @property({type: String})
- _selectedCommand = 'Clone';
+ private readonly userModel = getAppContext().userModel;
- @property({type: String})
- _selectedScheme?: string;
+ private readonly restApiService = getAppContext().restApiService;
- @property({type: Object})
- _schemesObj?: SchemesInfoMap;
-
- @property({type: Array})
- weblinks: WebLinkInfo[] = [];
-
- private readonly restApiService = appContext.restApiService;
+ constructor() {
+ super();
+ subscribe(this, this.userModel.preferences$, prefs => {
+ if (prefs?.download_scheme) {
+ // Note (issue 5180): normalize the download scheme with lower-case.
+ this.selectedScheme = prefs.download_scheme.toLowerCase();
+ }
+ });
+ }
override connectedCallback() {
super.connectedCallback();
- this._loadRepo();
fireTitleChange(this, `${this.repo}`);
}
- _computePluginData(
- configRecord: PolymerDeepPropertyChange<
- PluginNameToPluginParametersMap,
- PluginNameToPluginParametersMap
- >
- ) {
- if (!configRecord || !configRecord.base) {
- return [];
- }
+ static override get styles() {
+ return [
+ fontStyles,
+ formStyles,
+ subpageStyles,
+ sharedStyles,
+ css`
+ .info {
+ margin-bottom: var(--spacing-xl);
+ }
+ h2.edited:after {
+ color: var(--deemphasized-text-color);
+ content: ' *';
+ }
+ .loading,
+ .hide {
+ display: none;
+ }
+ #loading.loading {
+ display: block;
+ }
+ #loading:not(.loading) {
+ display: none;
+ }
+ #options .repositorySettings {
+ display: none;
+ }
+ #options .repositorySettings.showConfig {
+ display: block;
+ }
+ `,
+ ];
+ }
- const pluginConfig = configRecord.base;
+ override render() {
+ const configChanged = this.hasConfigChanged();
+ return html`
+ <div class="main gr-form-styles read-only">
+ <div class="info">
+ <h1 id="Title" class="heading-1">${this.repo}</h1>
+ <hr />
+ <div>
+ <a href=${this.weblinks?.[0]?.url}
+ ><gr-button link ?disabled=${!this.weblinks?.[0]?.url}
+ >Browse</gr-button
+ ></a
+ ><a href=${this.computeChangesUrl(this.repo)}
+ ><gr-button link>View Changes</gr-button></a
+ >
+ </div>
+ </div>
+ <div id="loading" class=${this.loading ? 'loading' : ''}>
+ Loading...
+ </div>
+ <div id="loadedContent" class=${this.loading ? 'loading' : ''}>
+ ${this.renderDownloadCommands()}
+ <h2
+ id="configurations"
+ class="heading-2 ${configChanged ? 'edited' : ''}"
+ >
+ Configurations
+ </h2>
+ <div id="form">
+ <fieldset>
+ ${this.renderDescription()} ${this.renderRepoOptions()}
+ ${this.renderPluginConfig()}
+ <gr-button
+ ?disabled=${this.readOnly || !configChanged}
+ @click=${this.handleSaveRepoConfig}
+ >Save changes</gr-button
+ >
+ </fieldset>
+ <gr-endpoint-decorator name="repo-config">
+ <gr-endpoint-param
+ name="repoName"
+ .value=${this.repo}
+ ></gr-endpoint-param>
+ <gr-endpoint-param
+ name="readOnly"
+ .value=${this.readOnly}
+ ></gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </div>
+ </div>
+ </div>
+ `;
+ }
+
+ private renderDownloadCommands() {
+ return html`
+ <div
+ id="downloadContent"
+ class=${!this.schemes || !this.schemes.length ? 'hide' : ''}
+ >
+ <h2 id="download" class="heading-2">Download</h2>
+ <fieldset>
+ <gr-download-commands
+ id="downloadCommands"
+ .commands=${this.computeCommands(
+ this.repo,
+ this.schemesObj,
+ this.selectedScheme
+ )}
+ .schemes=${this.schemes}
+ .selectedScheme=${this.selectedScheme}
+ @selected-scheme-changed=${this.handleSelectedSchemeValueChanged}
+ ></gr-download-commands>
+ </fieldset>
+ </div>
+ `;
+ }
+
+ private renderDescription() {
+ return html`
+ <h3 id="Description" class="heading-3">Description</h3>
+ <fieldset>
+ <gr-textarea
+ id="descriptionInput"
+ class="description"
+ autocomplete="on"
+ placeholder="<Insert repo description here>"
+ rows="4"
+ monospace
+ ?disabled=${this.readOnly}
+ .text=${this.repoConfig?.description}
+ @text-changed=${this.handleDescriptionTextChanged}
+ ></gr-textarea>
+ </fieldset>
+ `;
+ }
+
+ private renderRepoOptions() {
+ return html`
+ <h3 id="Options" class="heading-3">Repository Options</h3>
+ <fieldset id="options">
+ ${this.renderState()} ${this.renderSubmitType()}
+ ${this.renderContentMerges()} ${this.renderNewChange()}
+ ${this.renderChangeId()} ${this.renderEnableSignedPush()}
+ ${this.renderRequireSignedPush()} ${this.renderRejectImplicitMerges()}
+ ${this.renderUnRegisteredCc()} ${this.renderPrivateByDefault()}
+ ${this.renderWorkInProgressByDefault()} ${this.renderMaxGitObjectSize()}
+ ${this.renderMatchAuthoredDateWithCommitterDate()}
+ ${this.renderRejectEmptyCommit()}
+ </fieldset>
+ <h3 id="Options" class="heading-3">Contributor Agreements</h3>
+ <fieldset id="agreements">
+ ${this.renderContributorAgreement()} ${this.renderUseSignedOffBy()}
+ </fieldset>
+ `;
+ }
+
+ private renderState() {
+ return html`
+ <section>
+ <span class="title">State</span>
+ <span class="value">
+ <gr-select
+ id="stateSelect"
+ .bindValue=${this.repoConfig?.state}
+ @bind-value-changed=${this.handleStateSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.states.map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderSubmitType() {
+ return html`
+ <section>
+ <span class="title">Submit type</span>
+ <span class="value">
+ <gr-select
+ id="submitTypeSelect"
+ .bindValue=${this.repoConfig?.submit_type}
+ @bind-value-changed=${this.handleSubmitTypeSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatSubmitTypeSelect(this.repoConfig).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderContentMerges() {
+ return html`
+ <section>
+ <span class="title">Allow content merges</span>
+ <span class="value">
+ <gr-select
+ id="contentMergeSelect"
+ .bindValue=${this.repoConfig?.use_content_merge?.configured_value}
+ @bind-value-changed=${this.handleContentMergeSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.use_content_merge
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderNewChange() {
+ return html`
+ <section>
+ <span class="title">
+ Create a new change for every commit not in the target branch
+ </span>
+ <span class="value">
+ <gr-select
+ id="newChangeSelect"
+ .bindValue=${this.repoConfig
+ ?.create_new_change_for_all_not_in_target?.configured_value}
+ @bind-value-changed=${this.handleNewChangeSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.create_new_change_for_all_not_in_target
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderChangeId() {
+ return html`
+ <section>
+ <span class="title">Require Change-Id in commit message</span>
+ <span class="value">
+ <gr-select
+ id="requireChangeIdSelect"
+ .bindValue=${this.repoConfig?.require_change_id?.configured_value}
+ @bind-value-changed=${this
+ .handleRequireChangeIdSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.require_change_id
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderEnableSignedPush() {
+ return html`
+ <section
+ id="enableSignedPushSettings"
+ class="repositorySettings ${this.repoConfig?.enable_signed_push
+ ? 'showConfig'
+ : ''}"
+ >
+ <span class="title">Enable signed push</span>
+ <span class="value">
+ <gr-select
+ id="enableSignedPush"
+ .bindValue=${this.repoConfig?.enable_signed_push?.configured_value}
+ @bind-value-changed=${this.handleEnableSignedPushBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.enable_signed_push
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderRequireSignedPush() {
+ return html`
+ <section
+ id="requireSignedPushSettings"
+ class="repositorySettings ${this.repoConfig?.require_signed_push
+ ? 'showConfig'
+ : ''}"
+ >
+ <span class="title">Require signed push</span>
+ <span class="value">
+ <gr-select
+ id="requireSignedPush"
+ .bindValue=${this.repoConfig?.require_signed_push?.configured_value}
+ @bind-value-changed=${this.handleRequireSignedPushBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.require_signed_push
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderRejectImplicitMerges() {
+ return html`
+ <section>
+ <span class="title">
+ Reject implicit merges when changes are pushed for review</span
+ >
+ <span class="value">
+ <gr-select
+ id="rejectImplicitMergesSelect"
+ .bindValue=${this.repoConfig?.reject_implicit_merges
+ ?.configured_value}
+ @bind-value-changed=${this
+ .handleRejectImplicitMergeSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.reject_implicit_merges
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderUnRegisteredCc() {
+ return html`
+ <section>
+ <span class="title">
+ Enable adding unregistered users as reviewers and CCs on changes</span
+ >
+ <span class="value">
+ <gr-select
+ id="unRegisteredCcSelect"
+ .bindValue=${this.repoConfig?.enable_reviewer_by_email
+ ?.configured_value}
+ @bind-value-changed=${this
+ .handleUnRegisteredCcSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.enable_reviewer_by_email
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderPrivateByDefault() {
+ return html`
+ <section>
+ <span class="title"> Set all new changes private by default</span>
+ <span class="value">
+ <gr-select
+ id="setAllnewChangesPrivateByDefaultSelect"
+ .bindValue=${this.repoConfig?.private_by_default?.configured_value}
+ @bind-value-changed=${this
+ .handleSetAllNewChangesPrivateByDefaultSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.private_by_default
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderWorkInProgressByDefault() {
+ return html`
+ <section>
+ <span class="title">
+ Set new changes to "work in progress" by default</span
+ >
+ <span class="value">
+ <gr-select
+ id="setAllNewChangesWorkInProgressByDefaultSelect"
+ .bindValue=${this.repoConfig?.work_in_progress_by_default
+ ?.configured_value}
+ @bind-value-changed=${this
+ .handleSetAllNewChangesWorkInProgressByDefaultSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.work_in_progress_by_default
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderMaxGitObjectSize() {
+ return html`
+ <section>
+ <span class="title">Maximum Git object size limit</span>
+ <span class="value">
+ <iron-input
+ id="maxGitObjSizeIronInput"
+ .bindValue=${this.repoConfig?.max_object_size_limit
+ ?.configured_value}
+ @bind-value-changed=${this.handleMaxGitObjSizeBindValueChanged}
+ >
+ <input
+ id="maxGitObjSizeInput"
+ type="text"
+ ?disabled=${this.readOnly}
+ />
+ </iron-input>
+ ${this.repoConfig?.max_object_size_limit?.value
+ ? `effective: ${this.repoConfig.max_object_size_limit.value} bytes`
+ : ''}
+ </span>
+ </section>
+ `;
+ }
+
+ private renderMatchAuthoredDateWithCommitterDate() {
+ return html`
+ <section>
+ <span class="title"
+ >Match authored date with committer date upon submit</span
+ >
+ <span class="value">
+ <gr-select
+ id="matchAuthoredDateWithCommitterDateSelect"
+ .bindValue=${this.repoConfig?.match_author_to_committer_date
+ ?.configured_value}
+ @bind-value-changed=${this
+ .handleMatchAuthoredDateWithCommitterDateSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.match_author_to_committer_date
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderRejectEmptyCommit() {
+ return html`
+ <section>
+ <span class="title">Reject empty commit upon submit</span>
+ <span class="value">
+ <gr-select
+ id="rejectEmptyCommitSelect"
+ .bindValue=${this.repoConfig?.reject_empty_commit?.configured_value}
+ @bind-value-changed=${this
+ .handleRejectEmptyCommitSelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.reject_empty_commit
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderContributorAgreement() {
+ return html`
+ <section>
+ <span class="title">
+ Require a valid contributor agreement to upload</span
+ >
+ <span class="value">
+ <gr-select
+ id="contributorAgreementSelect"
+ .bindValue=${this.repoConfig?.use_contributor_agreements
+ ?.configured_value}
+ @bind-value-changed=${this
+ .handleUseContributorAgreementsBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.use_contributor_agreements
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderUseSignedOffBy() {
+ return html`
+ <section>
+ <span class="title">Require Signed-off-by in commit message</span>
+ <span class="value">
+ <gr-select
+ id="useSignedOffBySelect"
+ .bindValue=${this.repoConfig?.use_signed_off_by?.configured_value}
+ @bind-value-changed=${this
+ .handleUseSignedOffBySelectBindValueChanged}
+ >
+ <select ?disabled=${this.readOnly}>
+ ${this.formatBooleanSelect(
+ this.repoConfig?.use_signed_off_by
+ ).map(
+ item => html`
+ <option value=${item.value}>${item.label}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ `;
+ }
+
+ private renderPluginConfig() {
+ const pluginData = this.computePluginData();
+ return html` <div
+ class="pluginConfig ${!pluginData || !pluginData.length ? 'hide' : ''}"
+ @plugin-config-changed=${this.handlePluginConfigChanged}
+ >
+ <h3 class="heading-3">Plugins</h3>
+ ${pluginData.map(
+ item => html`
+ <gr-repo-plugin-config
+ .pluginData=${item}
+ ?disabled=${this.readOnly}
+ ></gr-repo-plugin-config>
+ `
+ )}
+ </div>`;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('repo')) {
+ this.loadRepo();
+ }
+ if (changedProperties.has('schemesObj')) {
+ this.computeSchemesAndDefault();
+ }
+ }
+
+ // private but used in test
+ computePluginData() {
+ if (!this.repoConfig || !this.repoConfig.plugin_config) return [];
+ const pluginConfig = this.repoConfig.plugin_config;
return Object.keys(pluginConfig).map(name => {
return {name, config: pluginConfig[name]};
});
}
- _loadRepo() {
- if (!this.repo) {
- return Promise.resolve();
- }
+ // private but used in test
+ async loadRepo() {
+ if (!this.repo) return Promise.resolve();
const promises = [];
@@ -175,8 +772,7 @@
};
promises.push(
- this._getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
+ this.restApiService.getLoggedIn().then(loggedIn => {
if (loggedIn) {
const repo = this.repo;
if (!repo) throw new Error('undefined repo');
@@ -190,71 +786,60 @@
}
// If the user is not an owner, is_owner is not a property.
- this._readOnly = !access[repo]?.is_owner;
+ this.readOnly = !access[repo]?.is_owner;
});
}
})
);
- promises.push(
- this.restApiService.getProjectConfig(this.repo, errFn).then(config => {
- if (!config) {
- return;
- }
+ const repoConfigHelper = async () => {
+ const config = await this.restApiService.getProjectConfig(
+ this.repo as RepoName,
+ errFn
+ );
+ if (!config) return;
- if (config.default_submit_type) {
- // The gr-select is bound to submit_type, which needs to be the
- // *configured* submit type. When default_submit_type is
- // present, the server reports the *effective* submit type in
- // submit_type, so we need to overwrite it before storing the
- // config in this.
- config.submit_type = config.default_submit_type.configured_value;
- }
- if (!config.state) {
- config.state = STATES.active.value;
- }
- this._repoConfig = config;
- this._loading = false;
- })
- );
-
- promises.push(
- this.restApiService.getConfig().then(config => {
- if (!config) {
- return;
- }
-
- this._schemesObj = config.download.schemes;
- })
- );
-
- return Promise.all(promises);
- }
-
- _computeLoadingClass(loading: boolean) {
- return loading ? 'loading' : '';
- }
-
- _computeHideClass(arr?: PluginData[] | string[]) {
- return !arr || !arr.length ? 'hide' : '';
- }
-
- _loggedInChanged(_loggedIn?: boolean) {
- if (!_loggedIn) {
- return;
- }
- this.restApiService.getPreferences().then(prefs => {
- if (prefs?.download_scheme) {
- // Note (issue 5180): normalize the download scheme with lower-case.
- this._selectedScheme = prefs.download_scheme.toLowerCase();
+ if (config.default_submit_type) {
+ // The gr-select is bound to submit_type, which needs to be the
+ // *configured* submit type. When default_submit_type is
+ // present, the server reports the *effective* submit type in
+ // submit_type, so we need to overwrite it before storing the
+ // config in this.
+ config.submit_type = config.default_submit_type.configured_value;
}
- });
+ if (!config.state) {
+ config.state = STATES.active.value as ProjectState;
+ }
+ // To properly check if the config has changed we need it to be a string
+ // as it's converted to a string in the input.
+ if (config.description === undefined) {
+ config.description = '';
+ }
+ // To properly check if the config has changed we need it to be a string
+ // as it's converted to a string in the input.
+ if (config.max_object_size_limit.configured_value === undefined) {
+ config.max_object_size_limit.configured_value = '';
+ }
+ this.repoConfig = config;
+ this.originalConfig = deepClone(config) as ConfigInfo;
+ this.loading = false;
+ };
+ promises.push(repoConfigHelper());
+
+ const configHelper = async () => {
+ const config = await this.restApiService.getConfig();
+ if (!config) return;
+
+ this.schemesObj = config.download.schemes;
+ };
+ promises.push(configHelper());
+
+ await Promise.all(promises);
}
- _formatBooleanSelect(item: InheritedBooleanInfo) {
- if (!item) {
- return;
- }
+ // private but used in test
+ formatBooleanSelect(item?: InheritedBooleanInfo) {
+ if (!item) return [];
let inheritLabel = 'Inherit';
if (!(item.inherited_value === undefined)) {
inheritLabel = `Inherit (${item.inherited_value})`;
@@ -275,12 +860,10 @@
];
}
- _formatSubmitTypeSelect(projectConfig: ConfigInfo) {
- if (!projectConfig) {
- return;
- }
+ private formatSubmitTypeSelect(repoConfig?: ConfigInfo) {
+ if (!repoConfig) return [];
const allValues = Object.values(SUBMIT_TYPES);
- const type = projectConfig.default_submit_type;
+ const type = repoConfig.default_submit_type;
if (!type) {
// Server is too old to report default_submit_type, so assume INHERIT
// is not a valid value.
@@ -306,15 +889,9 @@
];
}
- _isLoading() {
- return this._loading || this._loading === undefined;
- }
-
- _getLoggedIn() {
- return this.restApiService.getLoggedIn();
- }
-
- _formatRepoConfigForSave(repoConfig: ConfigInfo): ConfigInput {
+ // private but used in test
+ formatRepoConfigForSave(repoConfig?: ConfigInfo): ConfigInput {
+ if (!repoConfig) return {};
const configInputObj: ConfigInput = {};
for (const configKey of Object.keys(repoConfig)) {
const key = configKey as keyof ConfigInfo;
@@ -329,7 +906,7 @@
} else if (typeof repoConfig[key] === 'object') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const repoConfigObj: any = repoConfig[key];
- if (repoConfigObj.configured_value) {
+ if (repoConfigObj.configured_value !== undefined) {
configInputObj[key as keyof ConfigInput] =
repoConfigObj.configured_value;
}
@@ -341,56 +918,173 @@
return configInputObj;
}
- _handleSaveRepoConfig() {
- if (!this._repoConfig || !this.repo)
+ // private but used in test
+ async handleSaveRepoConfig() {
+ if (!this.repoConfig || !this.repo)
return Promise.reject(new Error('undefined repoConfig or repo'));
- return this.restApiService
- .saveRepoConfig(
- this.repo,
- this._formatRepoConfigForSave(this._repoConfig)
+ await this.restApiService.saveRepoConfig(
+ this.repo,
+ this.formatRepoConfigForSave(this.repoConfig)
+ );
+ this.originalConfig = deepClone(this.repoConfig) as ConfigInfo;
+ this.pluginConfigChanged = false;
+ return;
+ }
+
+ private isEdited(
+ original?: InheritedBooleanInfo | MaxObjectSizeLimitInfo,
+ repo?: InheritedBooleanInfo | MaxObjectSizeLimitInfo
+ ) {
+ return original?.configured_value !== repo?.configured_value;
+ }
+
+ private hasConfigChanged() {
+ const {repoConfig, originalConfig} = this;
+
+ if (!repoConfig || !originalConfig) return false;
+
+ if (originalConfig.description !== repoConfig.description) {
+ return true;
+ }
+ if (originalConfig.state !== repoConfig.state) {
+ return true;
+ }
+ if (originalConfig.submit_type !== repoConfig.submit_type) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.use_content_merge,
+ repoConfig.use_content_merge
)
- .then(() => {
- this._configChanged = false;
- });
- }
-
- @observe('_repoConfig.*')
- _handleConfigChanged() {
- if (this._isLoading()) {
- return;
+ ) {
+ return true;
}
- this._configChanged = true;
- }
-
- _computeButtonDisabled(readOnly: boolean, configChanged: boolean) {
- return readOnly || !configChanged;
- }
-
- _computeHeaderClass(configChanged: boolean) {
- return configChanged ? 'edited' : '';
- }
-
- _computeSchemes(schemesDefault: string[], schemesObj?: SchemesInfoMap) {
- return !schemesObj ? schemesDefault : Object.keys(schemesObj);
- }
-
- _schemesChanged(schemes: string[]) {
- if (schemes.length === 0) {
- return;
+ if (
+ this.isEdited(
+ originalConfig.create_new_change_for_all_not_in_target,
+ repoConfig.create_new_change_for_all_not_in_target
+ )
+ ) {
+ return true;
}
- if (!this._selectedScheme || !schemes.includes(this._selectedScheme)) {
- this._selectedScheme = schemes.sort()[0];
+ if (
+ this.isEdited(
+ originalConfig.require_change_id,
+ repoConfig.require_change_id
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.enable_signed_push,
+ repoConfig.enable_signed_push
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.require_signed_push,
+ repoConfig.require_signed_push
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.reject_implicit_merges,
+ repoConfig.reject_implicit_merges
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.enable_reviewer_by_email,
+ repoConfig.enable_reviewer_by_email
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.private_by_default,
+ repoConfig.private_by_default
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.work_in_progress_by_default,
+ repoConfig.work_in_progress_by_default
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.max_object_size_limit,
+ repoConfig.max_object_size_limit
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.match_author_to_committer_date,
+ repoConfig.match_author_to_committer_date
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.reject_empty_commit,
+ repoConfig.reject_empty_commit
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.use_contributor_agreements,
+ repoConfig.use_contributor_agreements
+ )
+ ) {
+ return true;
+ }
+ if (
+ this.isEdited(
+ originalConfig.use_signed_off_by,
+ repoConfig.use_signed_off_by
+ )
+ ) {
+ return true;
+ }
+
+ return this.pluginConfigChanged;
+ }
+
+ private computeSchemesAndDefault() {
+ this.schemes = !this.schemesObj ? [] : Object.keys(this.schemesObj).sort();
+ if (this.schemes.length > 0) {
+ if (!this.selectedScheme || !this.schemes.includes(this.selectedScheme)) {
+ this.selectedScheme = this.schemes.sort()[0];
+ }
}
}
- _computeCommands(
+ private computeCommands(
repo?: RepoName,
schemesObj?: SchemesInfoMap,
- _selectedScheme?: string
+ selectedScheme?: string
) {
- if (!schemesObj || !repo || !_selectedScheme) return [];
- if (!hasOwnProperty(schemesObj, _selectedScheme)) return [];
- const commandObj = schemesObj[_selectedScheme].clone_commands;
+ if (!schemesObj || !repo || !selectedScheme) return [];
+ if (!hasOwnProperty(schemesObj, selectedScheme)) return [];
+ const commandObj = schemesObj[selectedScheme].clone_commands;
const commands = [];
for (const [title, command] of Object.entries(commandObj)) {
commands.push({
@@ -406,36 +1100,171 @@
return commands;
}
- _computeRepositoriesClass(config: InheritedBooleanInfo) {
- return config ? 'showConfig' : '';
+ private computeChangesUrl(name?: RepoName) {
+ if (!name) return '';
+ return GerritNav.getUrlForProjectChanges(name as RepoName);
}
- _computeChangesUrl(name: RepoName) {
- return GerritNav.getUrlForProjectChanges(name);
- }
-
- _computeBrowseUrl(weblinks: WebLinkInfo[]) {
- return weblinks?.[0]?.url;
- }
-
- _handlePluginConfigChanged({
- detail: {name, config, notifyPath},
+ // private but used in test
+ handlePluginConfigChanged({
+ detail: {name, config},
}: {
detail: {
name: string;
config: PluginParameterToConfigParameterInfoMap;
- notifyPath: string;
};
}) {
- if (this._repoConfig?.plugin_config) {
- this._repoConfig.plugin_config[name] = config;
- this.notifyPath('_repoConfig.plugin_config.' + notifyPath);
+ if (this.repoConfig?.plugin_config) {
+ this.repoConfig.plugin_config[name] = config;
+ this.pluginConfigChanged = true;
+ this.requestUpdate();
}
}
-}
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-repo': GrRepo;
+ private handleSelectedSchemeValueChanged(e: CustomEvent) {
+ if (this.loading) return;
+ this.selectedScheme = e.detail.value;
+ }
+
+ private handleDescriptionTextChanged(e: CustomEvent) {
+ if (!this.repoConfig || this.loading) return;
+ this.repoConfig = {
+ ...this.repoConfig,
+ description: e.detail.value,
+ };
+ this.requestUpdate();
+ }
+
+ private handleStateSelectBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.repoConfig || this.loading) return;
+ this.repoConfig = {
+ ...this.repoConfig,
+ state: e.detail.value as ProjectState,
+ };
+ this.requestUpdate();
+ }
+
+ private handleSubmitTypeSelectBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.repoConfig || this.loading) return;
+ this.repoConfig = {
+ ...this.repoConfig,
+ submit_type: e.detail.value as SubmitType,
+ };
+ this.requestUpdate();
+ }
+
+ private handleContentMergeSelectBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.repoConfig?.use_content_merge || this.loading) return;
+ this.repoConfig.use_content_merge.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleNewChangeSelectBindValueChanged(e: BindValueChangeEvent) {
+ if (
+ !this.repoConfig?.create_new_change_for_all_not_in_target ||
+ this.loading
+ )
+ return;
+ this.repoConfig.create_new_change_for_all_not_in_target.configured_value = e
+ .detail.value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleRequireChangeIdSelectBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.repoConfig?.require_change_id || this.loading) return;
+ this.repoConfig.require_change_id.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleEnableSignedPushBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.repoConfig?.enable_signed_push || this.loading) return;
+ this.repoConfig.enable_signed_push.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleRequireSignedPushBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.repoConfig?.require_signed_push || this.loading) return;
+ this.repoConfig.require_signed_push.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleRejectImplicitMergeSelectBindValueChanged(
+ e: BindValueChangeEvent
+ ) {
+ if (!this.repoConfig?.reject_implicit_merges || this.loading) return;
+ this.repoConfig.reject_implicit_merges.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleUnRegisteredCcSelectBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.repoConfig?.enable_reviewer_by_email || this.loading) return;
+ this.repoConfig.enable_reviewer_by_email.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleSetAllNewChangesPrivateByDefaultSelectBindValueChanged(
+ e: BindValueChangeEvent
+ ) {
+ if (!this.repoConfig?.private_by_default || this.loading) return;
+ this.repoConfig.private_by_default.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleSetAllNewChangesWorkInProgressByDefaultSelectBindValueChanged(
+ e: BindValueChangeEvent
+ ) {
+ if (!this.repoConfig?.work_in_progress_by_default || this.loading) return;
+ this.repoConfig.work_in_progress_by_default.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleMaxGitObjSizeBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.repoConfig?.max_object_size_limit || this.loading) return;
+ this.repoConfig.max_object_size_limit.value = e.detail.value;
+ this.repoConfig.max_object_size_limit.configured_value = e.detail.value;
+ this.requestUpdate();
+ }
+
+ private handleMatchAuthoredDateWithCommitterDateSelectBindValueChanged(
+ e: BindValueChangeEvent
+ ) {
+ if (!this.repoConfig?.match_author_to_committer_date || this.loading)
+ return;
+ this.repoConfig.match_author_to_committer_date.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleRejectEmptyCommitSelectBindValueChanged(
+ e: BindValueChangeEvent
+ ) {
+ if (!this.repoConfig?.reject_empty_commit || this.loading) return;
+ this.repoConfig.reject_empty_commit.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleUseContributorAgreementsBindValueChanged(
+ e: BindValueChangeEvent
+ ) {
+ if (!this.repoConfig?.use_contributor_agreements || this.loading) return;
+ this.repoConfig.use_contributor_agreements.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
+ }
+
+ private handleUseSignedOffBySelectBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.repoConfig?.use_signed_off_by || this.loading) return;
+ this.repoConfig.use_signed_off_by.configured_value = e.detail
+ .value as InheritedBooleanInfoConfiguredValue;
+ this.requestUpdate();
}
}
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.ts b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.ts
deleted file mode 100644
index 3dbdefe..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.ts
+++ /dev/null
@@ -1,450 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-font-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-subpage-styles">
- .info {
- margin-bottom: var(--spacing-xl);
- }
- h2.edited:after {
- color: var(--deemphasized-text-color);
- content: ' *';
- }
- .loading,
- .hide {
- display: none;
- }
- #loading.loading {
- display: block;
- }
- #loading:not(.loading) {
- display: none;
- }
- #options .repositorySettings {
- display: none;
- }
- #options .repositorySettings.showConfig {
- display: block;
- }
- </style>
- <style include="gr-form-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <div class="main gr-form-styles read-only">
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <div class="info">
- <h1 id="Title" class="heading-1">[[repo]]</h1>
- <hr />
- <div>
- <a href$="[[_computeBrowseUrl(weblinks)]]"
- ><gr-button link disabled="[[!_computeBrowseUrl(weblinks)]]"
- >Browse</gr-button
- ></a
- ><a href$="[[_computeChangesUrl(repo)]]"
- ><gr-button link>View Changes</gr-button></a
- >
- </div>
- </div>
- <div id="loading" class$="[[_computeLoadingClass(_loading)]]">
- Loading...
- </div>
- <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
- <div id="downloadContent" class$="[[_computeHideClass(_schemes)]]">
- <h2 id="download" class="heading-2">Download</h2>
- <fieldset>
- <gr-download-commands
- id="downloadCommands"
- commands="[[_computeCommands(repo, _schemesObj, _selectedScheme)]]"
- schemes="[[_schemes]]"
- selected-scheme="{{_selectedScheme}}"
- ></gr-download-commands>
- </fieldset>
- </div>
- <h2
- id="configurations"
- class$="heading-2 [[_computeHeaderClass(_configChanged)]]"
- >
- Configurations
- </h2>
- <div id="form">
- <fieldset>
- <h3 id="Description" class="heading-3">Description</h3>
- <fieldset>
- <iron-autogrow-textarea
- id="descriptionInput"
- class="description"
- autocomplete="on"
- placeholder="<Insert repo description here>"
- bind-value="{{_repoConfig.description}}"
- disabled$="[[_readOnly]]"
- ></iron-autogrow-textarea>
- </fieldset>
- <h3 id="Options" class="heading-3">Repository Options</h3>
- <fieldset id="options">
- <section>
- <span class="title">State</span>
- <span class="value">
- <gr-select id="stateSelect" bind-value="{{_repoConfig.state}}">
- <select disabled$="[[_readOnly]]">
- <template is="dom-repeat" items="[[_states]]">
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Submit type</span>
- <span class="value">
- <gr-select
- id="submitTypeSelect"
- bind-value="{{_repoConfig.submit_type}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatSubmitTypeSelect(_repoConfig)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Allow content merges</span>
- <span class="value">
- <gr-select
- id="contentMergeSelect"
- bind-value="{{_repoConfig.use_content_merge.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.use_content_merge)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">
- Create a new change for every commit not in the target branch
- </span>
- <span class="value">
- <gr-select
- id="newChangeSelect"
- bind-value="{{_repoConfig.create_new_change_for_all_not_in_target.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.create_new_change_for_all_not_in_target)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Require Change-Id in commit message</span>
- <span class="value">
- <gr-select
- id="requireChangeIdSelect"
- bind-value="{{_repoConfig.require_change_id.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.require_change_id)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section
- id="enableSignedPushSettings"
- class$="repositorySettings [[_computeRepositoriesClass(_repoConfig.enable_signed_push)]]"
- >
- <span class="title">Enable signed push</span>
- <span class="value">
- <gr-select
- id="enableSignedPush"
- bind-value="{{_repoConfig.enable_signed_push.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.enable_signed_push)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section
- id="requireSignedPushSettings"
- class$="repositorySettings [[_computeRepositoriesClass(_repoConfig.require_signed_push)]]"
- >
- <span class="title">Require signed push</span>
- <span class="value">
- <gr-select
- id="requireSignedPush"
- bind-value="{{_repoConfig.require_signed_push.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.require_signed_push)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">
- Reject implicit merges when changes are pushed for review</span
- >
- <span class="value">
- <gr-select
- id="rejectImplicitMergesSelect"
- bind-value="{{_repoConfig.reject_implicit_merges.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.reject_implicit_merges)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">
- Enable adding unregistered users as reviewers and CCs on
- changes</span
- >
- <span class="value">
- <gr-select
- id="unRegisteredCcSelect"
- bind-value="{{_repoConfig.enable_reviewer_by_email.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.enable_reviewer_by_email)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title"> Set all new changes private by default</span>
- <span class="value">
- <gr-select
- id="setAllnewChangesPrivateByDefaultSelect"
- bind-value="{{_repoConfig.private_by_default.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.private_by_default)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">
- Set new changes to "work in progress" by default</span
- >
- <span class="value">
- <gr-select
- id="setAllNewChangesWorkInProgressByDefaultSelect"
- bind-value="{{_repoConfig.work_in_progress_by_default.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.work_in_progress_by_default)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Maximum Git object size limit</span>
- <span class="value">
- <iron-input
- id="maxGitObjSizeIronInput"
- bind-value="{{_repoConfig.max_object_size_limit.configured_value}}"
- type="text"
- disabled$="[[_readOnly]]"
- >
- <input
- id="maxGitObjSizeInput"
- bind-value="{{_repoConfig.max_object_size_limit.configured_value}}"
- is="iron-input"
- type="text"
- disabled$="[[_readOnly]]"
- />
- </iron-input>
- <template
- is="dom-if"
- if="[[_repoConfig.max_object_size_limit.value]]"
- >
- effective: [[_repoConfig.max_object_size_limit.value]] bytes
- </template>
- </span>
- </section>
- <section>
- <span class="title"
- >Match authored date with committer date upon submit</span
- >
- <span class="value">
- <gr-select
- id="matchAuthoredDateWithCommitterDateSelect"
- bind-value="{{_repoConfig.match_author_to_committer_date.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.match_author_to_committer_date)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Reject empty commit upon submit</span>
- <span class="value">
- <gr-select
- id="rejectEmptyCommitSelect"
- bind-value="{{_repoConfig.reject_empty_commit.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.reject_empty_commit)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- </fieldset>
- <h3 id="Options" class="heading-3">Contributor Agreements</h3>
- <fieldset id="agreements">
- <section>
- <span class="title">
- Require a valid contributor agreement to upload</span
- >
- <span class="value">
- <gr-select
- id="contributorAgreementSelect"
- bind-value="{{_repoConfig.use_contributor_agreements.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.use_contributor_agreements)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <section>
- <span class="title">Require Signed-off-by in commit message</span>
- <span class="value">
- <gr-select
- id="useSignedOffBySelect"
- bind-value="{{_repoConfig.use_signed_off_by.configured_value}}"
- >
- <select disabled$="[[_readOnly]]">
- <template
- is="dom-repeat"
- items="[[_formatBooleanSelect(_repoConfig.use_signed_off_by)]]"
- >
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- </fieldset>
- <div
- class$="pluginConfig [[_computeHideClass(_pluginData)]]"
- on-plugin-config-changed="_handlePluginConfigChanged"
- >
- <h3 class="heading-3">Plugins</h3>
- <template is="dom-repeat" items="[[_pluginData]]" as="data">
- <gr-repo-plugin-config
- plugin-data="[[data]]"
- disabled$="[[_readOnly]]"
- ></gr-repo-plugin-config>
- </template>
- </div>
- <gr-button
- on-click="_handleSaveRepoConfig"
- disabled$="[[_computeButtonDisabled(_readOnly, _configChanged)]]"
- >Save changes</gr-button
- >
- </fieldset>
- <gr-endpoint-decorator name="repo-config">
- <gr-endpoint-param
- name="repoName"
- value="[[repo]]"
- ></gr-endpoint-param>
- <gr-endpoint-param
- name="readOnly"
- value="[[_readOnly]]"
- ></gr-endpoint-param>
- </gr-endpoint-decorator>
- </div>
- </div>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.js
deleted file mode 100644
index 89ad86e..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.js
+++ /dev/null
@@ -1,358 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-repo.js';
-import {mockPromise} from '../../../test/test-utils.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {addListenerForTest, stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-repo');
-
-suite('gr-repo tests', () => {
- let element;
- let loggedInStub;
- let repoStub;
- const repoConf = {
- description: 'Access inherited by all other projects.',
- use_contributor_agreements: {
- value: false,
- configured_value: 'FALSE',
- },
- use_content_merge: {
- value: false,
- configured_value: 'FALSE',
- },
- use_signed_off_by: {
- value: false,
- configured_value: 'FALSE',
- },
- create_new_change_for_all_not_in_target: {
- value: false,
- configured_value: 'FALSE',
- },
- require_change_id: {
- value: false,
- configured_value: 'FALSE',
- },
- enable_signed_push: {
- value: false,
- configured_value: 'FALSE',
- },
- require_signed_push: {
- value: false,
- configured_value: 'FALSE',
- },
- reject_implicit_merges: {
- value: false,
- configured_value: 'FALSE',
- },
- private_by_default: {
- value: false,
- configured_value: 'FALSE',
- },
- match_author_to_committer_date: {
- value: false,
- configured_value: 'FALSE',
- },
- reject_empty_commit: {
- value: false,
- configured_value: 'FALSE',
- },
- enable_reviewer_by_email: {
- value: false,
- configured_value: 'FALSE',
- },
- max_object_size_limit: {},
- submit_type: 'MERGE_IF_NECESSARY',
- default_submit_type: {
- value: 'MERGE_IF_NECESSARY',
- configured_value: 'INHERIT',
- inherited_value: 'MERGE_IF_NECESSARY',
- },
- };
-
- const REPO = 'test-repo';
- const SCHEMES = {http: {}, repo: {}, ssh: {}};
-
- function getFormFields() {
- const selects = Array.from(
- element.root.querySelectorAll('select'));
- const textareas = Array.from(
- element.root.querySelectorAll('iron-autogrow-textarea'));
- const inputs = Array.from(
- element.root.querySelectorAll('input'));
- return inputs.concat(textareas).concat(selects);
- }
-
- setup(() => {
- loggedInStub = stubRestApi('getLoggedIn').returns(Promise.resolve(false));
- stubRestApi('getConfig').returns(Promise.resolve({download: {}}));
- repoStub =
- stubRestApi('getProjectConfig').returns(Promise.resolve(repoConf));
- element = basicFixture.instantiate();
- });
-
- test('_computePluginData', () => {
- assert.deepEqual(element._computePluginData(), []);
- assert.deepEqual(element._computePluginData({}), []);
- assert.deepEqual(element._computePluginData({base: {}}), []);
- assert.deepEqual(element._computePluginData({base: {plugin: 'data'}}),
- [{name: 'plugin', config: 'data'}]);
- });
-
- test('_handlePluginConfigChanged', () => {
- const notifyStub = sinon.stub(element, 'notifyPath');
- element._repoConfig = {plugin_config: {}};
- element._handlePluginConfigChanged({detail: {
- name: 'test',
- config: 'data',
- notifyPath: 'path',
- }});
- flush();
-
- assert.equal(element._repoConfig.plugin_config.test, 'data');
- assert.equal(notifyStub.lastCall.args[0],
- '_repoConfig.plugin_config.path');
- });
-
- test('loading displays before repo config is loaded', () => {
- assert.isTrue(element.$.loading.classList.contains('loading'));
- assert.isFalse(getComputedStyle(element.$.loading).display === 'none');
- assert.isTrue(element.$.loadedContent.classList.contains('loading'));
- assert.isTrue(getComputedStyle(element.$.loadedContent)
- .display === 'none');
- });
-
- test('download commands visibility', () => {
- element._loading = false;
- flush();
- assert.isTrue(element.$.downloadContent.classList.contains('hide'));
- assert.isTrue(getComputedStyle(element.$.downloadContent)
- .display == 'none');
- element._schemesObj = SCHEMES;
- flush();
- assert.isFalse(element.$.downloadContent.classList.contains('hide'));
- assert.isFalse(getComputedStyle(element.$.downloadContent)
- .display == 'none');
- });
-
- test('form defaults to read only', () => {
- assert.isTrue(element._readOnly);
- });
-
- test('form defaults to read only when not logged in', async () => {
- element.repo = REPO;
- await element._loadRepo();
- assert.isTrue(element._readOnly);
- });
-
- test('form defaults to read only when logged in and not admin', async () => {
- element.repo = REPO;
- stubRestApi('getRepoAccess')
- .callsFake(() => Promise.resolve({'test-repo': {}}));
- await element._loadRepo();
- assert.isTrue(element._readOnly);
- });
-
- test('all form elements are disabled when not admin', async () => {
- element.repo = REPO;
- await element._loadRepo();
- flush();
- const formFields = getFormFields();
- for (const field of formFields) {
- assert.isTrue(field.hasAttribute('disabled'));
- }
- });
-
- test('_formatBooleanSelect', () => {
- let item = {inherited_value: true};
- assert.deepEqual(element._formatBooleanSelect(item), [
- {
- label: 'Inherit (true)',
- value: 'INHERIT',
- },
- {
- label: 'True',
- value: 'TRUE',
- }, {
- label: 'False',
- value: 'FALSE',
- },
- ]);
-
- item = {inherited_value: false};
- assert.deepEqual(element._formatBooleanSelect(item), [
- {
- label: 'Inherit (false)',
- value: 'INHERIT',
- },
- {
- label: 'True',
- value: 'TRUE',
- }, {
- label: 'False',
- value: 'FALSE',
- },
- ]);
-
- // For items without inherited values
- item = {};
- assert.deepEqual(element._formatBooleanSelect(item), [
- {
- label: 'Inherit',
- value: 'INHERIT',
- },
- {
- label: 'True',
- value: 'TRUE',
- }, {
- label: 'False',
- value: 'FALSE',
- },
- ]);
- });
-
- test('fires page-error', async () => {
- repoStub.restore();
-
- element.repo = 'test';
-
- const pageErrorFired = mockPromise();
- const response = {status: 404};
- stubRestApi('getProjectConfig').callsFake((repo, errFn) => {
- errFn(response);
- return Promise.resolve(undefined);
- });
- addListenerForTest(document, 'page-error', e => {
- assert.deepEqual(e.detail.response, response);
- pageErrorFired.resolve();
- });
-
- element._loadRepo();
- await pageErrorFired;
- });
-
- suite('admin', () => {
- setup(() => {
- element.repo = REPO;
- loggedInStub.returns(Promise.resolve(true));
- stubRestApi('getRepoAccess')
- .returns(Promise.resolve({'test-repo': {is_owner: true}}));
- });
-
- test('all form elements are enabled', async () => {
- await element._loadRepo();
- await flush();
- const formFields = getFormFields();
- for (const field of formFields) {
- assert.isFalse(field.hasAttribute('disabled'));
- }
- assert.isFalse(element._loading);
- });
-
- test('state gets set correctly', async () => {
- await element._loadRepo();
- assert.equal(element._repoConfig.state, 'ACTIVE');
- assert.equal(element.$.stateSelect.bindValue, 'ACTIVE');
- });
-
- test('inherited submit type value is calculated correctly', async () => {
- await element._loadRepo();
- const sel = element.$.submitTypeSelect;
- assert.equal(sel.bindValue, 'INHERIT');
- assert.equal(
- sel.nativeSelect.options[0].text,
- 'Inherit (Merge if necessary)'
- );
- });
-
- test('fields update and save correctly', async () => {
- const configInputObj = {
- description: 'new description',
- use_contributor_agreements: 'TRUE',
- use_content_merge: 'TRUE',
- use_signed_off_by: 'TRUE',
- create_new_change_for_all_not_in_target: 'TRUE',
- require_change_id: 'TRUE',
- enable_signed_push: 'TRUE',
- require_signed_push: 'TRUE',
- reject_implicit_merges: 'TRUE',
- private_by_default: 'TRUE',
- match_author_to_committer_date: 'TRUE',
- reject_empty_commit: 'TRUE',
- max_object_size_limit: 10,
- submit_type: 'FAST_FORWARD_ONLY',
- state: 'READ_ONLY',
- enable_reviewer_by_email: 'TRUE',
- };
-
- const saveStub = stubRestApi('saveRepoConfig')
- .callsFake(() => Promise.resolve({}));
-
- const button = element.root.querySelectorAll('gr-button')[2];
-
- await element._loadRepo();
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- element.$.descriptionInput.bindValue = configInputObj.description;
- element.$.stateSelect.bindValue = configInputObj.state;
- element.$.submitTypeSelect.bindValue = configInputObj.submit_type;
- element.$.contentMergeSelect.bindValue =
- configInputObj.use_content_merge;
- element.$.newChangeSelect.bindValue =
- configInputObj.create_new_change_for_all_not_in_target;
- element.$.requireChangeIdSelect.bindValue =
- configInputObj.require_change_id;
- element.$.enableSignedPush.bindValue =
- configInputObj.enable_signed_push;
- element.$.requireSignedPush.bindValue =
- configInputObj.require_signed_push;
- element.$.rejectImplicitMergesSelect.bindValue =
- configInputObj.reject_implicit_merges;
- element.$.setAllnewChangesPrivateByDefaultSelect.bindValue =
- configInputObj.private_by_default;
- element.$.matchAuthoredDateWithCommitterDateSelect.bindValue =
- configInputObj.match_author_to_committer_date;
- const inputElement = PolymerElement ?
- element.$.maxGitObjSizeIronInput : element.$.maxGitObjSizeInput;
- inputElement.bindValue = configInputObj.max_object_size_limit;
- element.$.contributorAgreementSelect.bindValue =
- configInputObj.use_contributor_agreements;
- element.$.useSignedOffBySelect.bindValue =
- configInputObj.use_signed_off_by;
- element.$.rejectEmptyCommitSelect.bindValue =
- configInputObj.reject_empty_commit;
- element.$.unRegisteredCcSelect.bindValue =
- configInputObj.enable_reviewer_by_email;
-
- assert.isFalse(button.hasAttribute('disabled'));
- assert.isTrue(element.$.configurations.classList.contains('edited'));
-
- const formattedObj =
- element._formatRepoConfigForSave(element._repoConfig);
- assert.deepEqual(formattedObj, configInputObj);
-
- await element._handleSaveRepoConfig();
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- assert.isTrue(saveStub.lastCall.calledWithExactly(REPO,
- configInputObj));
- });
- });
-});
-
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
new file mode 100644
index 0000000..82338d3
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.ts
@@ -0,0 +1,570 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-repo';
+import {GrRepo} from './gr-repo';
+import {mockPromise} from '../../../test/test-utils';
+import {
+ addListenerForTest,
+ queryAll,
+ queryAndAssert,
+ stubRestApi,
+} from '../../../test/test-utils';
+import {
+ createInheritedBoolean,
+ createServerInfo,
+} from '../../../test/test-data-generators';
+import {
+ ConfigInfo,
+ GitRef,
+ GroupId,
+ GroupName,
+ InheritedBooleanInfo,
+ MaxObjectSizeLimitInfo,
+ PluginParameterToConfigParameterInfoMap,
+ ProjectAccessGroups,
+ ProjectAccessInfoMap,
+ RepoName,
+} from '../../../types/common';
+import {
+ ConfigParameterInfoType,
+ InheritedBooleanInfoConfiguredValue,
+ ProjectState,
+ SubmitType,
+} from '../../../constants/constants';
+import {
+ createConfig,
+ createDownloadSchemes,
+} from '../../../test/test-data-generators';
+import {PageErrorEvent} from '../../../types/events.js';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {GrSelect} from '../../shared/gr-select/gr-select';
+import {GrTextarea} from '../../shared/gr-textarea/gr-textarea';
+import {IronInputElement} from '@polymer/iron-input/iron-input';
+
+const basicFixture = fixtureFromElement('gr-repo');
+
+suite('gr-repo tests', () => {
+ let element: GrRepo;
+ let loggedInStub: sinon.SinonStub;
+ let repoStub: sinon.SinonStub;
+
+ const repoConf: ConfigInfo = {
+ description: 'Access inherited by all other projects.',
+ use_contributor_agreements: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ use_content_merge: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ use_signed_off_by: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ create_new_change_for_all_not_in_target: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ require_change_id: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ enable_signed_push: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ require_signed_push: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ reject_implicit_merges: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ match_author_to_committer_date: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ reject_empty_commit: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ enable_reviewer_by_email: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ private_by_default: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ work_in_progress_by_default: {
+ value: false,
+ configured_value: InheritedBooleanInfoConfiguredValue.FALSE,
+ },
+ max_object_size_limit: {},
+ commentlinks: {},
+ submit_type: SubmitType.MERGE_IF_NECESSARY,
+ default_submit_type: {
+ value: SubmitType.MERGE_IF_NECESSARY,
+ configured_value: SubmitType.INHERIT,
+ inherited_value: SubmitType.MERGE_IF_NECESSARY,
+ },
+ };
+
+ const REPO = 'test-repo';
+ const SCHEMES = {
+ ...createDownloadSchemes(),
+ http: {
+ url: 'test',
+ is_auth_required: false,
+ is_auth_supported: false,
+ commands: 'test',
+ clone_commands: {clone: 'test'},
+ },
+ repo: {
+ url: 'test',
+ is_auth_required: false,
+ is_auth_supported: false,
+ commands: 'test',
+ clone_commands: {clone: 'test'},
+ },
+ ssh: {
+ url: 'test',
+ is_auth_required: false,
+ is_auth_supported: false,
+ commands: 'test',
+ clone_commands: {clone: 'test'},
+ },
+ };
+
+ function getFormFields() {
+ const selects = Array.from(queryAll(element, 'select'));
+ const textareas = Array.from(queryAll(element, 'iron-autogrow-textarea'));
+ const inputs = Array.from(queryAll(element, 'input'));
+ return inputs.concat(textareas).concat(selects);
+ }
+
+ setup(async () => {
+ loggedInStub = stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+ stubRestApi('getConfig').returns(Promise.resolve(createServerInfo()));
+ repoStub = stubRestApi('getProjectConfig').returns(
+ Promise.resolve(repoConf)
+ );
+ element = basicFixture.instantiate();
+ await element.updateComplete;
+ });
+
+ test('_computePluginData', async () => {
+ element.repoConfig = {
+ ...createConfig(),
+ plugin_config: {},
+ };
+ await element.updateComplete;
+ assert.deepEqual(element.computePluginData(), []);
+
+ element.repoConfig.plugin_config = {
+ 'test-plugin': {
+ test: {display_name: 'test plugin', type: 'STRING'},
+ } as PluginParameterToConfigParameterInfoMap,
+ };
+ await element.updateComplete;
+ assert.deepEqual(element.computePluginData(), [
+ {
+ name: 'test-plugin',
+ config: {
+ test: {
+ display_name: 'test plugin',
+ type: 'STRING' as ConfigParameterInfoType,
+ },
+ },
+ },
+ ]);
+ });
+
+ test('handlePluginConfigChanged', async () => {
+ const requestUpdateStub = sinon.stub(element, 'requestUpdate');
+ element.repoConfig = {
+ ...createConfig(),
+ plugin_config: {},
+ };
+ element.handlePluginConfigChanged({
+ detail: {
+ name: 'test',
+ config: {
+ test: {display_name: 'test plugin', type: 'STRING'},
+ } as PluginParameterToConfigParameterInfoMap,
+ },
+ });
+ await element.updateComplete;
+
+ assert.deepEqual(element.repoConfig!.plugin_config!.test, {
+ test: {display_name: 'test plugin', type: 'STRING'},
+ } as PluginParameterToConfigParameterInfoMap);
+ assert.isTrue(requestUpdateStub.called);
+ });
+
+ test('loading displays before repo config is loaded', () => {
+ assert.isTrue(
+ queryAndAssert<HTMLDivElement>(element, '#loading').classList.contains(
+ 'loading'
+ )
+ );
+ assert.isFalse(
+ getComputedStyle(queryAndAssert<HTMLDivElement>(element, '#loading'))
+ .display === 'none'
+ );
+ assert.isTrue(
+ queryAndAssert<HTMLDivElement>(
+ element,
+ '#loadedContent'
+ ).classList.contains('loading')
+ );
+ assert.isTrue(
+ getComputedStyle(
+ queryAndAssert<HTMLDivElement>(element, '#loadedContent')
+ ).display === 'none'
+ );
+ });
+
+ test('download commands visibility', async () => {
+ element.loading = false;
+ await element.updateComplete;
+ assert.isTrue(
+ queryAndAssert<HTMLDivElement>(
+ element,
+ '#downloadContent'
+ ).classList.contains('hide')
+ );
+ assert.isTrue(
+ getComputedStyle(
+ queryAndAssert<HTMLDivElement>(element, '#downloadContent')
+ ).display === 'none'
+ );
+ element.schemesObj = SCHEMES;
+ await element.updateComplete;
+ assert.isFalse(
+ queryAndAssert<HTMLDivElement>(
+ element,
+ '#downloadContent'
+ ).classList.contains('hide')
+ );
+ assert.isFalse(
+ getComputedStyle(
+ queryAndAssert<HTMLDivElement>(element, '#downloadContent')
+ ).display === 'none'
+ );
+ });
+
+ test('form defaults to read only', () => {
+ assert.isTrue(element.readOnly);
+ });
+
+ test('form defaults to read only when not logged in', async () => {
+ element.repo = REPO as RepoName;
+ await element.loadRepo();
+ assert.isTrue(element.readOnly);
+ });
+
+ test('form defaults to read only when logged in and not admin', async () => {
+ element.repo = REPO as RepoName;
+
+ stubRestApi('getRepoAccess').callsFake(() =>
+ Promise.resolve({
+ 'test-repo': {
+ revision: 'xxxx',
+ local: {
+ 'refs/*': {
+ permissions: {
+ owner: {rules: {xxx: {action: 'ALLOW', force: false}}},
+ },
+ },
+ },
+ owner_of: ['refs/*'] as GitRef[],
+ groups: {
+ xxxx: {
+ id: 'xxxx' as GroupId,
+ url: 'test',
+ name: 'test' as GroupName,
+ },
+ } as ProjectAccessGroups,
+ config_web_links: [{name: 'gitiles', url: 'test'}],
+ },
+ } as ProjectAccessInfoMap)
+ );
+ await element.loadRepo();
+ assert.isTrue(element.readOnly);
+ });
+
+ test('all form elements are disabled when not admin', async () => {
+ element.repo = REPO as RepoName;
+ await element.loadRepo();
+ await element.updateComplete;
+ const formFields = getFormFields();
+ for (const field of formFields) {
+ assert.isTrue(field.hasAttribute('disabled'));
+ }
+ });
+
+ test('formatBooleanSelect', () => {
+ let item: InheritedBooleanInfo = {
+ ...createInheritedBoolean(true),
+ inherited_value: true,
+ };
+ assert.deepEqual(element.formatBooleanSelect(item), [
+ {
+ label: 'Inherit (true)',
+ value: 'INHERIT',
+ },
+ {
+ label: 'True',
+ value: 'TRUE',
+ },
+ {
+ label: 'False',
+ value: 'FALSE',
+ },
+ ]);
+
+ item = {...createInheritedBoolean(false), inherited_value: false};
+ assert.deepEqual(element.formatBooleanSelect(item), [
+ {
+ label: 'Inherit (false)',
+ value: 'INHERIT',
+ },
+ {
+ label: 'True',
+ value: 'TRUE',
+ },
+ {
+ label: 'False',
+ value: 'FALSE',
+ },
+ ]);
+
+ // For items without inherited values
+ item = createInheritedBoolean(false);
+ assert.deepEqual(element.formatBooleanSelect(item), [
+ {
+ label: 'Inherit',
+ value: 'INHERIT',
+ },
+ {
+ label: 'True',
+ value: 'TRUE',
+ },
+ {
+ label: 'False',
+ value: 'FALSE',
+ },
+ ]);
+ });
+
+ test('fires page-error', async () => {
+ repoStub.restore();
+
+ element.repo = 'test' as RepoName;
+
+ const pageErrorFired = mockPromise();
+ const response = {...new Response(), status: 404};
+ stubRestApi('getProjectConfig').callsFake((_, errFn) => {
+ if (errFn !== undefined) {
+ errFn(response);
+ }
+ return Promise.resolve(undefined);
+ });
+ addListenerForTest(document, 'page-error', e => {
+ assert.deepEqual((e as PageErrorEvent).detail.response, response);
+ pageErrorFired.resolve();
+ });
+
+ element.loadRepo();
+ await pageErrorFired;
+ });
+
+ suite('admin', () => {
+ setup(() => {
+ element.repo = REPO as RepoName;
+ loggedInStub.returns(Promise.resolve(true));
+ stubRestApi('getRepoAccess').callsFake(() =>
+ Promise.resolve({
+ 'test-repo': {
+ revision: 'xxxx',
+ local: {
+ 'refs/*': {
+ permissions: {
+ owner: {rules: {xxx: {action: 'ALLOW', force: false}}},
+ },
+ },
+ },
+ is_owner: true,
+ owner_of: ['refs/*'] as GitRef[],
+ groups: {
+ xxxx: {
+ id: 'xxxx' as GroupId,
+ url: 'test',
+ name: 'test' as GroupName,
+ },
+ } as ProjectAccessGroups,
+ config_web_links: [{name: 'gitiles', url: 'test'}],
+ },
+ } as ProjectAccessInfoMap)
+ );
+ });
+
+ test('all form elements are enabled', async () => {
+ await element.loadRepo();
+ await element.updateComplete;
+ const formFields = getFormFields();
+ for (const field of formFields) {
+ assert.isFalse(field.hasAttribute('disabled'));
+ }
+ assert.isFalse(element.loading);
+ });
+
+ test('state gets set correctly', async () => {
+ await element.loadRepo();
+ assert.equal(element.repoConfig!.state, ProjectState.ACTIVE);
+ assert.equal(
+ queryAndAssert<GrSelect>(element, '#stateSelect').bindValue,
+ ProjectState.ACTIVE
+ );
+ });
+
+ test('inherited submit type value is calculated correctly', async () => {
+ await element.loadRepo();
+ const sel = queryAndAssert<GrSelect>(element, '#submitTypeSelect');
+ assert.equal(sel.bindValue, 'INHERIT');
+ assert.equal(
+ sel.nativeSelect.options[0].text,
+ 'Inherit (Merge if necessary)'
+ );
+ });
+
+ test('fields update and save correctly', async () => {
+ const configInputObj = {
+ description: 'new description',
+ use_contributor_agreements: InheritedBooleanInfoConfiguredValue.TRUE,
+ use_content_merge: InheritedBooleanInfoConfiguredValue.TRUE,
+ use_signed_off_by: InheritedBooleanInfoConfiguredValue.TRUE,
+ create_new_change_for_all_not_in_target:
+ InheritedBooleanInfoConfiguredValue.TRUE,
+ require_change_id: InheritedBooleanInfoConfiguredValue.TRUE,
+ enable_signed_push: InheritedBooleanInfoConfiguredValue.TRUE,
+ require_signed_push: InheritedBooleanInfoConfiguredValue.TRUE,
+ reject_implicit_merges: InheritedBooleanInfoConfiguredValue.TRUE,
+ private_by_default: InheritedBooleanInfoConfiguredValue.TRUE,
+ work_in_progress_by_default: InheritedBooleanInfoConfiguredValue.TRUE,
+ match_author_to_committer_date:
+ InheritedBooleanInfoConfiguredValue.TRUE,
+ reject_empty_commit: InheritedBooleanInfoConfiguredValue.TRUE,
+ max_object_size_limit: '10' as MaxObjectSizeLimitInfo,
+ submit_type: SubmitType.FAST_FORWARD_ONLY,
+ state: ProjectState.READ_ONLY,
+ enable_reviewer_by_email: InheritedBooleanInfoConfiguredValue.TRUE,
+ };
+
+ const saveStub = stubRestApi('saveRepoConfig').callsFake(() =>
+ Promise.resolve(new Response())
+ );
+
+ const button = queryAll<GrButton>(element, 'gr-button')[2];
+
+ await element.loadRepo();
+ assert.isTrue(button.hasAttribute('disabled'));
+ assert.isFalse(
+ queryAndAssert<HTMLHeadingElement>(
+ element,
+ '#Title'
+ ).classList.contains('edited')
+ );
+ queryAndAssert<GrTextarea>(element, '#descriptionInput').text =
+ configInputObj.description;
+ queryAndAssert<GrSelect>(element, '#stateSelect').bindValue =
+ configInputObj.state;
+ queryAndAssert<GrSelect>(element, '#submitTypeSelect').bindValue =
+ configInputObj.submit_type;
+ queryAndAssert<GrSelect>(element, '#contentMergeSelect').bindValue =
+ configInputObj.use_content_merge;
+ queryAndAssert<GrSelect>(element, '#newChangeSelect').bindValue =
+ configInputObj.create_new_change_for_all_not_in_target;
+ queryAndAssert<GrSelect>(element, '#requireChangeIdSelect').bindValue =
+ configInputObj.require_change_id;
+ queryAndAssert<GrSelect>(element, '#enableSignedPush').bindValue =
+ configInputObj.enable_signed_push;
+ queryAndAssert<GrSelect>(element, '#requireSignedPush').bindValue =
+ configInputObj.require_signed_push;
+ queryAndAssert<GrSelect>(
+ element,
+ '#rejectImplicitMergesSelect'
+ ).bindValue = configInputObj.reject_implicit_merges;
+ queryAndAssert<GrSelect>(
+ element,
+ '#setAllnewChangesPrivateByDefaultSelect'
+ ).bindValue = configInputObj.private_by_default;
+ queryAndAssert<GrSelect>(
+ element,
+ '#setAllNewChangesWorkInProgressByDefaultSelect'
+ ).bindValue = configInputObj.work_in_progress_by_default;
+ queryAndAssert<GrSelect>(
+ element,
+ '#matchAuthoredDateWithCommitterDateSelect'
+ ).bindValue = configInputObj.match_author_to_committer_date;
+ queryAndAssert<IronInputElement>(
+ element,
+ '#maxGitObjSizeIronInput'
+ ).bindValue = String(configInputObj.max_object_size_limit);
+ queryAndAssert<GrSelect>(
+ element,
+ '#contributorAgreementSelect'
+ ).bindValue = configInputObj.use_contributor_agreements;
+ queryAndAssert<GrSelect>(element, '#useSignedOffBySelect').bindValue =
+ configInputObj.use_signed_off_by;
+ queryAndAssert<GrSelect>(element, '#rejectEmptyCommitSelect').bindValue =
+ configInputObj.reject_empty_commit;
+ queryAndAssert<GrSelect>(element, '#unRegisteredCcSelect').bindValue =
+ configInputObj.enable_reviewer_by_email;
+
+ await element.updateComplete;
+
+ assert.isFalse(button.hasAttribute('disabled'));
+ assert.isTrue(
+ queryAndAssert<HTMLHeadingElement>(
+ element,
+ '#configurations'
+ ).classList.contains('edited')
+ );
+
+ const formattedObj = element.formatRepoConfigForSave(element.repoConfig);
+ assert.deepEqual(formattedObj, configInputObj);
+
+ await element.handleSaveRepoConfig();
+ assert.isTrue(button.hasAttribute('disabled'));
+ assert.isFalse(
+ queryAndAssert<HTMLHeadingElement>(
+ element,
+ '#Title'
+ ).classList.contains('edited')
+ );
+ assert.isTrue(
+ saveStub.lastCall.calledWithExactly(REPO as RepoName, configInputObj)
+ );
+ });
+ });
+});
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.ts b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.ts
index 172f807..4d0f2c4 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.ts
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.ts
@@ -15,16 +15,17 @@
* limitations under the License.
*/
import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
-import '../../../styles/gr-form-styles';
-import '../../../styles/shared-styles';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-select/gr-select';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-rule-editor_html';
import {encodeURL, getBaseUrl} from '../../../utils/url-util';
import {AccessPermissionId} from '../../../utils/access-util';
-import {property, customElement, observe} from '@polymer/decorators';
import {fireEvent} from '../../../utils/event-util';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, html, css} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+import {BindValueChangeEvent} from '../../../types/events';
+import {ifDefined} from 'lit/directives/if-defined';
/**
* Fired when the rule has been modified or removed.
@@ -71,7 +72,7 @@
];
interface Rule {
- value: RuleValue;
+ value?: RuleValue;
}
interface RuleValue {
@@ -100,18 +101,14 @@
}
@customElement('gr-rule-editor')
-export class GrRuleEditor extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrRuleEditor extends LitElement {
@property({type: Boolean})
hasRange?: boolean;
@property({type: Object})
label?: RuleLabel;
- @property({type: Boolean, observer: '_handleEditingChanged'})
+ @property({type: Boolean})
editing = false;
@property({type: String})
@@ -124,97 +121,283 @@
@property({type: String})
permission!: AccessPermissionId;
- @property({type: Object, notify: true})
+ @property({type: Object})
rule?: Rule;
@property({type: String})
section?: string;
- @property({type: Boolean})
- _deleted = false;
+ // private but used in test
+ @state() deleted = false;
- @property({type: Object})
- _originalRuleValues?: RuleValue;
+ // private but used in test
+ @state() originalRuleValues?: RuleValue;
constructor() {
super();
- this.addEventListener('access-saved', () => this._handleAccessSaved());
- }
-
- override ready() {
- super.ready();
- // Called on ready rather than the observer because when new rules are
- // added, the observer is triggered prior to being ready.
- if (!this.rule) {
- return;
- } // Check needed for test purposes.
- this._setupValues(this.rule);
+ this.addEventListener('access-saved', () => this.handleAccessSaved());
}
override connectedCallback() {
super.connectedCallback();
+ if (this.rule) {
+ this.setupValues();
+ }
// Check needed for test purposes.
- if (!this._originalRuleValues && this.rule) {
- // Observer _handleValueChange is called after the ready()
- // method finishes. Original values must be set later to
- // avoid set .modified flag to true
- this._setOriginalRuleValues(this.rule.value);
+ if (!this.originalRuleValues && this.rule) {
+ this.setOriginalRuleValues();
}
}
- _setupValues(rule: Rule) {
- if (!rule.value) {
- this._setDefaultRuleValues();
+ static override get styles() {
+ return [
+ formStyles,
+ sharedStyles,
+ css`
+ :host {
+ border-bottom: 1px solid var(--border-color);
+ padding: var(--spacing-m);
+ display: block;
+ }
+ #removeBtn {
+ display: none;
+ }
+ .editing #removeBtn {
+ display: flex;
+ }
+ #options {
+ align-items: baseline;
+ display: flex;
+ }
+ #options > * {
+ margin-right: var(--spacing-m);
+ }
+ #mainContainer {
+ align-items: baseline;
+ display: flex;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ }
+ #deletedContainer.deleted {
+ align-items: baseline;
+ display: flex;
+ justify-content: space-between;
+ }
+ #undoBtn,
+ #force,
+ #deletedContainer,
+ #mainContainer.deleted {
+ display: none;
+ }
+ #undoBtn.modified,
+ #force.force {
+ display: block;
+ }
+ .groupPath {
+ color: var(--deemphasized-text-color);
+ }
+ iron-autogrow-textarea {
+ width: 14em;
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`
+ <div
+ id="mainContainer"
+ class="gr-form-styles ${this.computeSectionClass()}"
+ >
+ <div id="options">
+ <gr-select
+ id="action"
+ .bindValue=${this.rule?.value?.action}
+ @bind-value-changed=${(e: BindValueChangeEvent) => {
+ this.handleActionBindValueChanged(e);
+ }}
+ >
+ <select ?disabled=${!this.editing}>
+ ${this.computeOptions().map(
+ item => html` <option value=${item}>${item}</option> `
+ )}
+ </select>
+ </gr-select>
+ ${this.renderMinAndMaxLabel()} ${this.renderMinAndMaxInput()}
+ <a
+ class="groupPath"
+ href="${ifDefined(this.computeGroupPath(this.groupId))}"
+ >
+ ${this.groupName}
+ </a>
+ <gr-select
+ id="force"
+ class="${this.computeForce(this.rule?.value?.action)
+ ? 'force'
+ : ''}"
+ .bindValue=${this.rule?.value?.force}
+ @bind-value-changed=${(e: BindValueChangeEvent) => {
+ this.handleForceBindValueChanged(e);
+ }}
+ >
+ <select ?disabled=${!this.editing}>
+ ${this.computeForceOptions(this.rule?.value?.action).map(
+ item => html`
+ <option value=${item.value}>${item.value}</option>
+ `
+ )}
+ </select>
+ </gr-select>
+ </div>
+ <gr-button
+ link
+ id="removeBtn"
+ @click=${() => {
+ this.handleRemoveRule();
+ }}
+ >Remove</gr-button
+ >
+ </div>
+ <div
+ id="deletedContainer"
+ class="gr-form-styles ${this.computeSectionClass()}"
+ >
+ ${this.groupName} was deleted
+ <gr-button
+ link
+ id="undoRemoveBtn"
+ @click=${() => {
+ this.handleUndoRemove();
+ }}
+ >Undo</gr-button
+ >
+ </div>
+ `;
+ }
+
+ private renderMinAndMaxLabel() {
+ if (!this.label) return;
+
+ return html`
+ <gr-select
+ id="labelMin"
+ .bindValue=${this.rule?.value?.min}
+ @bind-value-changed=${(e: BindValueChangeEvent) => {
+ this.handleMinBindValueChanged(e);
+ }}
+ >
+ <select ?disabled=${!this.editing}>
+ ${this.label.values.map(
+ item => html` <option value=${item.value}>${item.value}</option> `
+ )}
+ </select>
+ </gr-select>
+ <gr-select
+ id="labelMax"
+ .bindValue=${this.rule?.value?.max}
+ @bind-value-changed=${(e: BindValueChangeEvent) => {
+ this.handleMaxBindValueChanged(e);
+ }}
+ >
+ <select ?disabled=${!this.editing}>
+ ${this.label.values.map(
+ item => html` <option value=${item.value}>${item.value}</option> `
+ )}
+ </select>
+ </gr-select>
+ `;
+ }
+
+ private renderMinAndMaxInput() {
+ if (!this.hasRange) return;
+
+ return html`
+ <iron-autogrow-textarea
+ id="minInput"
+ class="min"
+ autocomplete="on"
+ placeholder="Min value"
+ .bindValue=${this.rule?.value?.min}
+ ?disabled=${!this.editing}
+ @bind-value-changed=${(e: BindValueChangeEvent) => {
+ this.handleMinBindValueChanged(e);
+ }}
+ ></iron-autogrow-textarea>
+ <iron-autogrow-textarea
+ id="maxInput"
+ class="max"
+ autocomplete="on"
+ placeholder="Max value"
+ .bindValue=${this.rule?.value?.max}
+ ?disabled=${!this.editing}
+ @bind-value-changed=${(e: BindValueChangeEvent) => {
+ this.handleMaxBindValueChanged(e);
+ }}
+ ></iron-autogrow-textarea>
+ `;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('editing')) {
+ this.handleEditingChanged(changedProperties.get('editing') as boolean);
}
}
- _computeForce(permission: AccessPermissionId, action: string) {
- if (AccessPermissionId.PUSH === permission && action !== Action.DENY) {
+ // private but used in test
+ setupValues() {
+ if (!this.rule?.value) {
+ this.setDefaultRuleValues();
+ }
+ }
+
+ // private but used in test
+ computeForce(action?: string) {
+ if (AccessPermissionId.PUSH === this.permission && action !== Action.DENY) {
return true;
}
- return AccessPermissionId.EDIT_TOPIC_NAME === permission;
+ return AccessPermissionId.EDIT_TOPIC_NAME === this.permission;
}
- _computeForceClass(permission: AccessPermissionId, action: string) {
- return this._computeForce(permission, action) ? 'force' : '';
+ // private but used in test
+ computeGroupPath(groupId?: string) {
+ if (!groupId) return;
+ return `${getBaseUrl()}/admin/groups/${encodeURL(groupId, true)}`;
}
- _computeGroupPath(group: string) {
- return `${getBaseUrl()}/admin/groups/${encodeURL(group, true)}`;
- }
-
- _handleAccessSaved() {
- if (!this.rule) return;
+ // private but used in test
+ handleAccessSaved() {
// Set a new 'original' value to keep track of after the value has been
// saved.
- this._setOriginalRuleValues(this.rule.value);
+ this.setOriginalRuleValues();
}
- _handleEditingChanged(editing: boolean, editingOld: boolean) {
+ private handleEditingChanged(editingOld: boolean) {
// Ignore when editing gets set initially.
if (!editingOld) {
return;
}
// Restore original values if no longer editing.
- if (!editing) {
- this._handleUndoChange();
+ if (!this.editing) {
+ this.handleUndoChange();
}
}
- _computeSectionClass(editing: boolean, deleted: boolean) {
+ // private but used in test
+ computeSectionClass() {
const classList = [];
- if (editing) {
+ if (this.editing) {
classList.push('editing');
}
- if (deleted) {
+ if (this.deleted) {
classList.push('deleted');
}
return classList.join(' ');
}
- _computeForceOptions(permission: string, action: string) {
- if (permission === AccessPermissionId.PUSH) {
+ // private but used in test
+ computeForceOptions(action?: string) {
+ if (this.permission === AccessPermissionId.PUSH) {
if (action === Action.ALLOW) {
return ForcePushOptions.ALLOW;
} else if (action === Action.BLOCK) {
@@ -222,82 +405,158 @@
} else {
return [];
}
- } else if (permission === AccessPermissionId.EDIT_TOPIC_NAME) {
+ } else if (this.permission === AccessPermissionId.EDIT_TOPIC_NAME) {
return FORCE_EDIT_OPTIONS;
}
return [];
}
- _getDefaultRuleValues(permission: AccessPermissionId, label?: RuleLabel) {
+ // private but used in test
+ getDefaultRuleValues() {
const ruleAction = Action.ALLOW;
const value: RuleValue = {};
- if (permission === AccessPermissionId.PRIORITY) {
+ if (this.permission === AccessPermissionId.PRIORITY) {
value.action = PRIORITY_OPTIONS[0];
return value;
- } else if (label) {
- value.min = label.values[0].value;
- value.max = label.values[label.values.length - 1].value;
- } else if (this._computeForce(permission, ruleAction)) {
- value.force = this._computeForceOptions(permission, ruleAction)[0].value;
+ } else if (this.label) {
+ value.min = this.label.values[0].value;
+ value.max = this.label.values[this.label.values.length - 1].value;
+ } else if (this.computeForce(ruleAction)) {
+ value.force = this.computeForceOptions(ruleAction)[0].value;
}
value.action = DROPDOWN_OPTIONS[0];
return value;
}
- _setDefaultRuleValues() {
- this.set(
- 'rule.value',
- this._getDefaultRuleValues(this.permission, this.label)
- );
+ // private but used in test
+ setDefaultRuleValues() {
+ this.rule!.value = this.getDefaultRuleValues();
+
+ this.handleRuleChange();
}
- _computeOptions(permission: string) {
- if (permission === 'priority') {
+ // private but used in test
+ computeOptions() {
+ if (this.permission === 'priority') {
return PRIORITY_OPTIONS;
}
return DROPDOWN_OPTIONS;
}
- _handleRemoveRule() {
- if (!this.rule) return;
+ private handleRemoveRule() {
+ if (!this.rule?.value) return;
if (this.rule.value.added) {
fireEvent(this, 'added-rule-removed');
}
- this._deleted = true;
+ this.deleted = true;
this.rule.value.deleted = true;
+
+ this.handleRuleChange();
+
fireEvent(this, 'access-modified');
}
- _handleUndoRemove() {
- if (!this.rule) return;
- this._deleted = false;
+ private handleUndoRemove() {
+ if (!this.rule?.value) return;
+ this.deleted = false;
delete this.rule.value.deleted;
+
+ this.handleRuleChange();
}
- _handleUndoChange() {
- if (!this.rule) return;
+ private handleUndoChange() {
+ if (!this.rule?.value) return;
// gr-permission will take care of removing rules that were added but
// unsaved. We need to keep the added bit for the filter.
if (this.rule.value.added) {
return;
}
- this.set('rule.value', {...this._originalRuleValues});
- this._deleted = false;
+ this.rule.value = {...this.originalRuleValues};
+ this.deleted = false;
delete this.rule.value.deleted;
delete this.rule.value.modified;
+
+ this.handleRuleChange();
}
- @observe('rule.value.*')
- _handleValueChange() {
- if (!this._originalRuleValues || !this.rule) {
+ // private but used in test
+ handleValueChange() {
+ if (!this.originalRuleValues || !this.rule?.value) {
return;
}
this.rule.value.modified = true;
+
+ this.handleRuleChange();
+
// Allows overall access page to know a change has been made.
fireEvent(this, 'access-modified');
}
- _setOriginalRuleValues(value: RuleValue) {
- this._originalRuleValues = {...value};
+ // private but used in test
+ setOriginalRuleValues() {
+ if (!this.rule?.value) return;
+ this.originalRuleValues = {...this.rule!.value};
+ }
+
+ private handleActionBindValueChanged(e: BindValueChangeEvent) {
+ if (
+ !this.rule?.value ||
+ e.detail.value === undefined ||
+ this.rule.value.action === String(e.detail.value)
+ )
+ return;
+
+ this.rule.value.action = String(e.detail.value);
+
+ this.handleValueChange();
+ }
+
+ private handleMinBindValueChanged(e: BindValueChangeEvent) {
+ if (
+ !this.rule?.value ||
+ e.detail.value === undefined ||
+ this.rule.value.min === Number(e.detail.value)
+ )
+ return;
+ this.rule.value.min = Number(e.detail.value);
+
+ this.handleValueChange();
+ }
+
+ private handleMaxBindValueChanged(e: BindValueChangeEvent) {
+ if (
+ !this.rule?.value ||
+ e.detail.value === undefined ||
+ this.rule.value.max === Number(e.detail.value)
+ )
+ return;
+ this.rule.value.max = Number(e.detail.value);
+
+ this.handleValueChange();
+ }
+
+ private handleForceBindValueChanged(e: BindValueChangeEvent) {
+ const forceValue = String(e.detail.value) === 'true' ? true : false;
+ if (
+ !this.rule?.value ||
+ e.detail.value === undefined ||
+ this.rule.value.force === forceValue
+ )
+ return;
+ this.rule.value.force = forceValue;
+
+ this.handleValueChange();
+ }
+
+ private handleRuleChange() {
+ this.requestUpdate('rule');
+
+ this.dispatchEvent(
+ new CustomEvent('rule-changed', {
+ detail: {value: this.rule},
+ composed: true,
+ bubbles: true,
+ })
+ );
}
}
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_html.ts b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_html.ts
deleted file mode 100644
index c4d7688..0000000
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_html.ts
+++ /dev/null
@@ -1,159 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- border-bottom: 1px solid var(--border-color);
- padding: var(--spacing-m);
- display: block;
- }
- #removeBtn {
- display: none;
- }
- .editing #removeBtn {
- display: flex;
- }
- #options {
- align-items: baseline;
- display: flex;
- }
- #options > * {
- margin-right: var(--spacing-m);
- }
- #mainContainer {
- align-items: baseline;
- display: flex;
- flex-wrap: nowrap;
- justify-content: space-between;
- }
- #deletedContainer.deleted {
- align-items: baseline;
- display: flex;
- justify-content: space-between;
- }
- #undoBtn,
- #force,
- #deletedContainer,
- #mainContainer.deleted {
- display: none;
- }
- #undoBtn.modified,
- #force.force {
- display: block;
- }
- .groupPath {
- color: var(--deemphasized-text-color);
- }
- </style>
- <style include="gr-form-styles">
- iron-autogrow-textarea {
- width: 14em;
- }
- </style>
- <div
- id="mainContainer"
- class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]"
- >
- <div id="options">
- <gr-select
- id="action"
- bind-value="{{rule.value.action}}"
- on-change="_handleValueChange"
- >
- <select disabled$="[[!editing]]">
- <template is="dom-repeat" items="[[_computeOptions(permission)]]">
- <option value="[[item]]">[[item]]</option>
- </template>
- </select>
- </gr-select>
- <template is="dom-if" if="[[label]]">
- <gr-select
- id="labelMin"
- bind-value="{{rule.value.min}}"
- on-change="_handleValueChange"
- >
- <select disabled$="[[!editing]]">
- <template is="dom-repeat" items="[[label.values]]">
- <option value="[[item.value]]">[[item.value]]</option>
- </template>
- </select>
- </gr-select>
- <gr-select
- id="labelMax"
- bind-value="{{rule.value.max}}"
- on-change="_handleValueChange"
- >
- <select disabled$="[[!editing]]">
- <template is="dom-repeat" items="[[label.values]]">
- <option value="[[item.value]]">[[item.value]]</option>
- </template>
- </select>
- </gr-select>
- </template>
- <template is="dom-if" if="[[hasRange]]">
- <iron-autogrow-textarea
- id="minInput"
- class="min"
- autocomplete="on"
- placeholder="Min value"
- bind-value="{{rule.value.min}}"
- disabled$="[[!editing]]"
- ></iron-autogrow-textarea>
- <iron-autogrow-textarea
- id="maxInput"
- class="max"
- autocomplete="on"
- placeholder="Max value"
- bind-value="{{rule.value.max}}"
- disabled$="[[!editing]]"
- ></iron-autogrow-textarea>
- </template>
- <a class="groupPath" href$="[[_computeGroupPath(groupId)]]">
- [[groupName]]
- </a>
- <gr-select
- id="force"
- class$="[[_computeForceClass(permission, rule.value.action)]]"
- bind-value="{{rule.value.force}}"
- on-change="_handleValueChange"
- >
- <select disabled$="[[!editing]]">
- <template
- is="dom-repeat"
- items="[[_computeForceOptions(permission, rule.value.action)]]"
- >
- <option value="[[item.value]]">[[item.name]]</option>
- </template>
- </select>
- </gr-select>
- </div>
- <gr-button link="" id="removeBtn" on-click="_handleRemoveRule"
- >Remove</gr-button
- >
- </div>
- <div
- id="deletedContainer"
- class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]"
- >
- [[groupName]] was deleted
- <gr-button link="" id="undoRemoveBtn" on-click="_handleUndoRemove"
- >Undo</gr-button
- >
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.js
deleted file mode 100644
index f3df132..0000000
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.js
+++ /dev/null
@@ -1,586 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-rule-editor.js';
-
-const basicFixture = fixtureFromElement('gr-rule-editor');
-
-suite('gr-rule-editor tests', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- });
-
- suite('unit tests', () => {
- test('_computeForce, _computeForceClass, and _computeForceOptions',
- () => {
- const ForcePushOptions = {
- ALLOW: [
- {name: 'Allow pushing (but not force pushing)', value: false},
- {name: 'Allow pushing with or without force', value: true},
- ],
- BLOCK: [
- {name: 'Block pushing with or without force', value: false},
- {name: 'Block force pushing', value: true},
- ],
- };
-
- const FORCE_EDIT_OPTIONS = [
- {
- name: 'No Force Edit',
- value: false,
- },
- {
- name: 'Force Edit',
- value: true,
- },
- ];
- let permission = 'push';
- let action = 'ALLOW';
- assert.isTrue(element._computeForce(permission, action));
- assert.equal(element._computeForceClass(permission, action),
- 'force');
- assert.deepEqual(element._computeForceOptions(permission, action),
- ForcePushOptions.ALLOW);
-
- action = 'BLOCK';
- assert.isTrue(element._computeForce(permission, action));
- assert.equal(element._computeForceClass(permission, action),
- 'force');
- assert.deepEqual(element._computeForceOptions(permission, action),
- ForcePushOptions.BLOCK);
-
- action = 'DENY';
- assert.isFalse(element._computeForce(permission, action));
- assert.equal(element._computeForceClass(permission, action), '');
- assert.equal(
- element._computeForceOptions(permission, action).length, 0);
-
- permission = 'editTopicName';
- assert.isTrue(element._computeForce(permission));
- assert.equal(element._computeForceClass(permission), 'force');
- assert.deepEqual(element._computeForceOptions(permission),
- FORCE_EDIT_OPTIONS);
- permission = 'submit';
- assert.isFalse(element._computeForce(permission));
- assert.equal(element._computeForceClass(permission), '');
- assert.deepEqual(element._computeForceOptions(permission), []);
- });
-
- test('_computeSectionClass', () => {
- let deleted = true;
- let editing = false;
- assert.equal(element._computeSectionClass(editing, deleted), 'deleted');
-
- deleted = false;
- assert.equal(element._computeSectionClass(editing, deleted), '');
-
- editing = true;
- assert.equal(element._computeSectionClass(editing, deleted), 'editing');
-
- deleted = true;
- assert.equal(element._computeSectionClass(editing, deleted),
- 'editing deleted');
- });
-
- test('_getDefaultRuleValues', () => {
- let permission = 'priority';
- let label;
- assert.deepEqual(element._getDefaultRuleValues(permission, label),
- {action: 'BATCH'});
- permission = 'label-Code-Review';
- label = {values: [
- {value: -2, text: 'This shall not be merged'},
- {value: -1, text: 'I would prefer this is not merged as is'},
- {value: -0, text: 'No score'},
- {value: 1, text: 'Looks good to me, but someone else must approve'},
- {value: 2, text: 'Looks good to me, approved'},
- ]};
- assert.deepEqual(element._getDefaultRuleValues(permission, label),
- {action: 'ALLOW', max: 2, min: -2});
- permission = 'push';
- label = undefined;
- assert.deepEqual(element._getDefaultRuleValues(permission, label),
- {action: 'ALLOW', force: false});
- permission = 'submit';
- assert.deepEqual(element._getDefaultRuleValues(permission, label),
- {action: 'ALLOW'});
- });
-
- test('_setDefaultRuleValues', () => {
- element.rule = {id: 123};
- const defaultValue = {action: 'ALLOW'};
- sinon.stub(element, '_getDefaultRuleValues').returns(defaultValue);
- element._setDefaultRuleValues();
- assert.isTrue(element._getDefaultRuleValues.called);
- assert.equal(element.rule.value, defaultValue);
- });
-
- test('_computeOptions', () => {
- const PRIORITY_OPTIONS = [
- 'BATCH',
- 'INTERACTIVE',
- ];
- const DROPDOWN_OPTIONS = [
- 'ALLOW',
- 'DENY',
- 'BLOCK',
- ];
- let permission = 'priority';
- assert.deepEqual(element._computeOptions(permission), PRIORITY_OPTIONS);
- permission = 'submit';
- assert.deepEqual(element._computeOptions(permission), DROPDOWN_OPTIONS);
- });
-
- test('_handleValueChange', () => {
- const modifiedHandler = sinon.stub();
- element.rule = {value: {}};
- element.addEventListener('access-modified', modifiedHandler);
- element._handleValueChange();
- assert.isNotOk(element.rule.value.modified);
- element._originalRuleValues = {};
- element._handleValueChange();
- assert.isTrue(element.rule.value.modified);
- assert.isTrue(modifiedHandler.called);
- });
-
- test('_handleAccessSaved', () => {
- const originalValue = {action: 'DENY'};
- const newValue = {action: 'ALLOW'};
- element._originalRuleValues = originalValue;
- element.rule = {value: newValue};
- element._handleAccessSaved();
- assert.deepEqual(element._originalRuleValues, newValue);
- });
-
- test('_setOriginalRuleValues', () => {
- const value = {
- action: 'ALLOW',
- force: false,
- };
- element._setOriginalRuleValues(value);
- assert.deepEqual(element._originalRuleValues, value);
- });
- });
-
- suite('already existing generic rule', () => {
- setup(async () => {
- element.group = 'Group Name';
- element.permission = 'submit';
- element.rule = {
- id: '123',
- value: {
- action: 'ALLOW',
- force: false,
- },
- };
- element.section = 'refs/*';
-
- // Typically called on ready since elements will have properties defined
- // by the parent element.
- element._setupValues(element.rule);
- await flush();
- element.connectedCallback();
- });
-
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- assert.deepEqual(element._originalRuleValues, element.rule.value);
- });
-
- test('values are set correctly', () => {
- assert.equal(element.$.action.bindValue, element.rule.value.action);
- assert.isNotOk(element.root.querySelector('#labelMin'));
- assert.isNotOk(element.root.querySelector('#labelMax'));
- assert.isFalse(element.$.force.classList.contains('force'));
- });
-
- test('modify and cancel restores original values', () => {
- element.editing = true;
- assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none');
- assert.isNotOk(element.rule.value.modified);
- element.$.action.bindValue = 'DENY';
- assert.isTrue(element.rule.value.modified);
- element.editing = false;
- assert.equal(getComputedStyle(element.$.removeBtn).display, 'none');
- assert.deepEqual(element._originalRuleValues, element.rule.value);
- assert.equal(element.$.action.bindValue, 'ALLOW');
- assert.isNotOk(element.rule.value.modified);
- });
-
- test('modify value', () => {
- assert.isNotOk(element.rule.value.modified);
- element.$.action.bindValue = 'DENY';
- flush();
- assert.isTrue(element.rule.value.modified);
-
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
-
- test('all selects are disabled when not in edit mode', () => {
- const selects = element.root.querySelectorAll('select');
- for (const select of selects) {
- assert.isTrue(select.disabled);
- }
- element.editing = true;
- for (const select of selects) {
- assert.isFalse(select.disabled);
- }
- });
-
- test('remove rule and undo remove', () => {
- element.editing = true;
- element.rule = {id: 123, value: {action: 'ALLOW'}};
- assert.isFalse(
- element.$.deletedContainer.classList.contains('deleted'));
- MockInteractions.tap(element.$.removeBtn);
- assert.isTrue(element.$.deletedContainer.classList.contains('deleted'));
- assert.isTrue(element._deleted);
- assert.isTrue(element.rule.value.deleted);
-
- MockInteractions.tap(element.$.undoRemoveBtn);
- assert.isFalse(element._deleted);
- assert.isNotOk(element.rule.value.deleted);
- });
-
- test('remove rule and cancel', () => {
- element.editing = true;
- assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none');
- assert.equal(getComputedStyle(element.$.deletedContainer).display,
- 'none');
-
- element.rule = {id: 123, value: {action: 'ALLOW'}};
- MockInteractions.tap(element.$.removeBtn);
- assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none');
- assert.notEqual(getComputedStyle(element.$.deletedContainer).display,
- 'none');
- assert.isTrue(element._deleted);
- assert.isTrue(element.rule.value.deleted);
-
- element.editing = false;
- assert.isFalse(element._deleted);
- assert.isNotOk(element.rule.value.deleted);
- assert.isNotOk(element.rule.value.modified);
-
- assert.deepEqual(element._originalRuleValues, element.rule.value);
- assert.equal(getComputedStyle(element.$.removeBtn).display, 'none');
- assert.equal(getComputedStyle(element.$.deletedContainer).display,
- 'none');
- });
-
- test('_computeGroupPath', () => {
- const group = '123';
- assert.equal(element._computeGroupPath(group),
- `/admin/groups/123`);
- });
- });
-
- suite('new edit rule', () => {
- setup(async () => {
- element.group = 'Group Name';
- element.permission = 'editTopicName';
- element.rule = {
- id: '123',
- };
- element.section = 'refs/*';
- element._setupValues(element.rule);
- await flush();
- element.rule.value.added = true;
- await flush();
- element.connectedCallback();
- });
-
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- // Since the element does not already have default values, they should
- // be set. The original values should be set to those too.
- assert.isNotOk(element.rule.value.modified);
- const expectedRuleValue = {
- action: 'ALLOW',
- force: false,
- added: true,
- };
- assert.deepEqual(element.rule.value, expectedRuleValue);
- test('values are set correctly', () => {
- assert.equal(element.$.action.bindValue, expectedRuleValue.action);
- assert.equal(element.$.force.bindValue, expectedRuleValue.action);
- });
- });
-
- test('modify value', () => {
- assert.isNotOk(element.rule.value.modified);
- element.$.force.bindValue = true;
- flush();
- assert.isTrue(element.rule.value.modified);
-
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
-
- test('remove value', () => {
- element.editing = true;
- const removeStub = sinon.stub();
- element.addEventListener('added-rule-removed', removeStub);
- MockInteractions.tap(element.$.removeBtn);
- flush();
- assert.isTrue(removeStub.called);
- });
- });
-
- suite('already existing rule with labels', () => {
- setup(async () => {
- element.label = {values: [
- {value: -2, text: 'This shall not be merged'},
- {value: -1, text: 'I would prefer this is not merged as is'},
- {value: -0, text: 'No score'},
- {value: 1, text: 'Looks good to me, but someone else must approve'},
- {value: 2, text: 'Looks good to me, approved'},
- ]};
- element.group = 'Group Name';
- element.permission = 'label-Code-Review';
- element.rule = {
- id: '123',
- value: {
- action: 'ALLOW',
- force: false,
- max: 2,
- min: -2,
- },
- };
- element.section = 'refs/*';
- element._setupValues(element.rule);
- await flush();
- element.connectedCallback();
- });
-
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- assert.deepEqual(element._originalRuleValues, element.rule.value);
- });
-
- test('values are set correctly', () => {
- assert.equal(element.$.action.bindValue, element.rule.value.action);
- assert.equal(
- element.root.querySelector('#labelMin').bindValue,
- element.rule.value.min);
- assert.equal(
- element.root.querySelector('#labelMax').bindValue,
- element.rule.value.max);
- assert.isFalse(element.$.force.classList.contains('force'));
- });
-
- test('modify value', () => {
- const removeStub = sinon.stub();
- element.addEventListener('added-rule-removed', removeStub);
- assert.isNotOk(element.rule.value.modified);
- element.root.querySelector('#labelMin').bindValue = 1;
- flush();
- assert.isTrue(element.rule.value.modified);
- assert.isFalse(removeStub.called);
-
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
- });
-
- suite('new rule with labels', () => {
- setup(async () => {
- sinon.spy(element, '_setDefaultRuleValues');
- element.label = {values: [
- {value: -2, text: 'This shall not be merged'},
- {value: -1, text: 'I would prefer this is not merged as is'},
- {value: -0, text: 'No score'},
- {value: 1, text: 'Looks good to me, but someone else must approve'},
- {value: 2, text: 'Looks good to me, approved'},
- ]};
- element.group = 'Group Name';
- element.permission = 'label-Code-Review';
- element.rule = {
- id: '123',
- };
- element.section = 'refs/*';
- element._setupValues(element.rule);
- await flush();
- element.rule.value.added = true;
- await flush();
- element.connectedCallback();
- });
-
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- // Since the element does not already have default values, they should
- // be set. The original values should be set to those too.
- assert.isNotOk(element.rule.value.modified);
- assert.isTrue(element._setDefaultRuleValues.called);
-
- const expectedRuleValue = {
- max: element.label.values[element.label.values.length - 1].value,
- min: element.label.values[0].value,
- action: 'ALLOW',
- added: true,
- };
- assert.deepEqual(element.rule.value, expectedRuleValue);
- test('values are set correctly', () => {
- assert.equal(
- element.$.action.bindValue,
- expectedRuleValue.action);
- assert.equal(
- element.root.querySelector('#labelMin').bindValue,
- expectedRuleValue.min);
- assert.equal(
- element.root.querySelector('#labelMax').bindValue,
- expectedRuleValue.max);
- });
- });
-
- test('modify value', () => {
- assert.isNotOk(element.rule.value.modified);
- element.root.querySelector('#labelMin').bindValue = 1;
- flush();
- assert.isTrue(element.rule.value.modified);
-
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
- });
-
- suite('already existing push rule', () => {
- setup(async () => {
- element.group = 'Group Name';
- element.permission = 'push';
- element.rule = {
- id: '123',
- value: {
- action: 'ALLOW',
- force: true,
- },
- };
- element.section = 'refs/*';
- element._setupValues(element.rule);
- await flush();
- element.connectedCallback();
- });
-
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- assert.deepEqual(element._originalRuleValues, element.rule.value);
- });
-
- test('values are set correctly', () => {
- assert.isTrue(element.$.force.classList.contains('force'));
- assert.equal(element.$.action.bindValue, element.rule.value.action);
- assert.equal(
- element.root.querySelector('#force').bindValue,
- element.rule.value.force);
- assert.isNotOk(element.root.querySelector('#labelMin'));
- assert.isNotOk(element.root.querySelector('#labelMax'));
- });
-
- test('modify value', () => {
- assert.isNotOk(element.rule.value.modified);
- element.$.action.bindValue = false;
- flush();
- assert.isTrue(element.rule.value.modified);
-
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
- });
-
- suite('new push rule', () => {
- setup(async () => {
- element.group = 'Group Name';
- element.permission = 'push';
- element.rule = {
- id: '123',
- };
- element.section = 'refs/*';
- element._setupValues(element.rule);
- await flush();
- element.rule.value.added = true;
- await flush();
- element.connectedCallback();
- });
-
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- // Since the element does not already have default values, they should
- // be set. The original values should be set to those too.
- assert.isNotOk(element.rule.value.modified);
- const expectedRuleValue = {
- action: 'ALLOW',
- force: false,
- added: true,
- };
- assert.deepEqual(element.rule.value, expectedRuleValue);
- test('values are set correctly', () => {
- assert.equal(element.$.action.bindValue, expectedRuleValue.action);
- assert.equal(element.$.force.bindValue, expectedRuleValue.action);
- });
- });
-
- test('modify value', () => {
- assert.isNotOk(element.rule.value.modified);
- element.$.force.bindValue = true;
- flush();
- assert.isTrue(element.rule.value.modified);
-
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
- });
-
- suite('already existing edit rule', () => {
- setup(async () => {
- element.group = 'Group Name';
- element.permission = 'editTopicName';
- element.rule = {
- id: '123',
- value: {
- action: 'ALLOW',
- force: true,
- },
- };
- element.section = 'refs/*';
- element._setupValues(element.rule);
- await flush();
- element.connectedCallback();
- });
-
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- assert.deepEqual(element._originalRuleValues, element.rule.value);
- });
-
- test('values are set correctly', () => {
- assert.isTrue(element.$.force.classList.contains('force'));
- assert.equal(element.$.action.bindValue, element.rule.value.action);
- assert.equal(
- element.root.querySelector('#force').bindValue,
- element.rule.value.force);
- assert.isNotOk(element.root.querySelector('#labelMin'));
- assert.isNotOk(element.root.querySelector('#labelMax'));
- });
-
- test('modify value', async () => {
- assert.isNotOk(element.rule.value.modified);
- element.$.action.bindValue = false;
- await flush();
- assert.isTrue(element.rule.value.modified);
-
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
- });
-});
-
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.ts b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.ts
new file mode 100644
index 0000000..47c3bea
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.ts
@@ -0,0 +1,684 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-rule-editor';
+import {GrRuleEditor} from './gr-rule-editor';
+import {AccessPermissionId} from '../../../utils/access-util';
+import {query, queryAll, queryAndAssert} from '../../../test/test-utils';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {GrSelect} from '../../shared/gr-select/gr-select';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+
+const basicFixture = fixtureFromElement('gr-rule-editor');
+
+suite('gr-rule-editor tests', () => {
+ let element: GrRuleEditor;
+
+ setup(async () => {
+ element = basicFixture.instantiate() as GrRuleEditor;
+ await element.updateComplete;
+ });
+
+ suite('unit tests', () => {
+ test('computeForce and computeForceOptions', () => {
+ const ForcePushOptions = {
+ ALLOW: [
+ {name: 'Allow pushing (but not force pushing)', value: false},
+ {name: 'Allow pushing with or without force', value: true},
+ ],
+ BLOCK: [
+ {name: 'Block pushing with or without force', value: false},
+ {name: 'Block force pushing', value: true},
+ ],
+ };
+
+ const FORCE_EDIT_OPTIONS = [
+ {
+ name: 'No Force Edit',
+ value: false,
+ },
+ {
+ name: 'Force Edit',
+ value: true,
+ },
+ ];
+ element.permission = 'push' as AccessPermissionId;
+ let action = 'ALLOW';
+ assert.isTrue(element.computeForce(action));
+ assert.deepEqual(
+ element.computeForceOptions(action),
+ ForcePushOptions.ALLOW
+ );
+
+ action = 'BLOCK';
+ assert.isTrue(element.computeForce(action));
+ assert.deepEqual(
+ element.computeForceOptions(action),
+ ForcePushOptions.BLOCK
+ );
+
+ action = 'DENY';
+ assert.isFalse(element.computeForce(action));
+ assert.equal(element.computeForceOptions(action).length, 0);
+
+ element.permission = 'editTopicName' as AccessPermissionId;
+ assert.isTrue(element.computeForce());
+ assert.deepEqual(element.computeForceOptions(), FORCE_EDIT_OPTIONS);
+ element.permission = 'submit' as AccessPermissionId;
+ assert.isFalse(element.computeForce());
+ assert.deepEqual(element.computeForceOptions(), []);
+ });
+
+ test('computeSectionClass', () => {
+ element.deleted = true;
+ element.editing = false;
+ assert.equal(element.computeSectionClass(), 'deleted');
+
+ element.deleted = false;
+ assert.equal(element.computeSectionClass(), '');
+
+ element.editing = true;
+ assert.equal(element.computeSectionClass(), 'editing');
+
+ element.deleted = true;
+ assert.equal(element.computeSectionClass(), 'editing deleted');
+ });
+
+ test('getDefaultRuleValues', () => {
+ element.permission = 'priority' as AccessPermissionId;
+ assert.deepEqual(element.getDefaultRuleValues(), {
+ action: 'BATCH',
+ });
+ element.permission = 'label-Code-Review' as AccessPermissionId;
+ element.label = {
+ values: [
+ {value: -2, text: 'This shall not be merged'},
+ {value: -1, text: 'I would prefer this is not merged as is'},
+ {value: -0, text: 'No score'},
+ {value: 1, text: 'Looks good to me, but someone else must approve'},
+ {value: 2, text: 'Looks good to me, approved'},
+ ],
+ };
+ assert.deepEqual(element.getDefaultRuleValues(), {
+ action: 'ALLOW',
+ max: 2,
+ min: -2,
+ });
+ element.permission = 'push' as AccessPermissionId;
+ element.label = undefined;
+ assert.deepEqual(element.getDefaultRuleValues(), {
+ action: 'ALLOW',
+ force: false,
+ });
+ element.permission = 'submit' as AccessPermissionId;
+ assert.deepEqual(element.getDefaultRuleValues(), {
+ action: 'ALLOW',
+ });
+ });
+
+ test('setDefaultRuleValues', async () => {
+ element.rule = {value: {}};
+ const defaultValue = {action: 'ALLOW'};
+ const getDefaultRuleValuesStub = sinon
+ .stub(element, 'getDefaultRuleValues')
+ .returns(defaultValue);
+ element.setDefaultRuleValues();
+ assert.isTrue(getDefaultRuleValuesStub.called);
+ assert.equal(element.rule!.value, defaultValue);
+ });
+
+ test('computeOptions', () => {
+ const PRIORITY_OPTIONS = ['BATCH', 'INTERACTIVE'];
+ const DROPDOWN_OPTIONS = ['ALLOW', 'DENY', 'BLOCK'];
+ element.permission = 'priority' as AccessPermissionId;
+ assert.deepEqual(element.computeOptions(), PRIORITY_OPTIONS);
+ element.permission = 'submit' as AccessPermissionId;
+ assert.deepEqual(element.computeOptions(), DROPDOWN_OPTIONS);
+ });
+
+ test('handleValueChange', () => {
+ const modifiedHandler = sinon.stub();
+ element.rule = {value: {}};
+ element.addEventListener('access-modified', modifiedHandler);
+ element.handleValueChange();
+ assert.isNotOk(element.rule!.value!.modified);
+ element.originalRuleValues = {};
+ element.handleValueChange();
+ assert.isTrue(element.rule!.value!.modified);
+ assert.isTrue(modifiedHandler.called);
+ });
+
+ test('handleAccessSaved', () => {
+ const originalValue = {action: 'DENY'};
+ const newValue = {action: 'ALLOW'};
+ element.originalRuleValues = originalValue;
+ element.rule = {value: newValue};
+ element.handleAccessSaved();
+ assert.deepEqual(element.originalRuleValues, newValue);
+ });
+
+ test('setOriginalRuleValues', () => {
+ element.rule = {
+ value: {
+ action: 'ALLOW',
+ force: false,
+ },
+ };
+ element.setOriginalRuleValues();
+ assert.deepEqual(element.originalRuleValues, element.rule.value);
+ });
+ });
+
+ suite('already existing generic rule', () => {
+ setup(async () => {
+ element.groupName = 'Group Name';
+ element.permission = 'submit' as AccessPermissionId;
+ element.rule = {
+ value: {
+ action: 'ALLOW',
+ force: false,
+ },
+ };
+ element.section = 'refs/*';
+ element.setupValues();
+ element.setOriginalRuleValues();
+ await element.updateComplete;
+ });
+
+ test('_ruleValues and originalRuleValues are set correctly', () => {
+ assert.deepEqual(element.originalRuleValues, element.rule!.value);
+ });
+
+ test('values are set correctly', () => {
+ assert.equal(
+ queryAndAssert<GrSelect>(element, '#action').bindValue,
+ element.rule!.value!.action
+ );
+ assert.isNotOk(query<GrSelect>(element, '#labelMin'));
+ assert.isNotOk(query<GrSelect>(element, '#labelMax'));
+ assert.isFalse(
+ queryAndAssert<GrSelect>(element, '#force').classList.contains('force')
+ );
+ });
+
+ test('modify and cancel restores original values', async () => {
+ element.rule = {value: {}};
+ element.editing = true;
+ await element.updateComplete;
+ assert.notEqual(
+ getComputedStyle(queryAndAssert<GrButton>(element, '#removeBtn'))
+ .display,
+ 'none'
+ );
+ assert.isNotOk(element.rule!.value!.modified);
+ const actionBindValue = queryAndAssert<GrSelect>(element, '#action');
+ actionBindValue.bindValue = 'DENY';
+ assert.isTrue(element.rule!.value!.modified);
+ element.editing = false;
+ await element.updateComplete;
+ assert.equal(
+ getComputedStyle(queryAndAssert<GrButton>(element, '#removeBtn'))
+ .display,
+ 'none'
+ );
+ assert.deepEqual(element.originalRuleValues, element.rule!.value);
+ assert.equal(
+ queryAndAssert<GrSelect>(element, '#action').bindValue,
+ 'ALLOW'
+ );
+ assert.isNotOk(element.rule!.value!.modified);
+ });
+
+ test('modify value', async () => {
+ assert.isNotOk(element.rule!.value!.modified);
+ const actionBindValue = queryAndAssert<GrSelect>(element, '#action');
+ actionBindValue.bindValue = 'DENY';
+ await element.updateComplete;
+ assert.isTrue(element.rule!.value!.modified);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element.originalRuleValues, element.rule!.value);
+ });
+
+ test('all selects are disabled when not in edit mode', async () => {
+ const selects = queryAll<HTMLSelectElement>(element, 'select');
+ for (const select of selects) {
+ assert.isTrue(select.disabled);
+ }
+ element.editing = true;
+ await element.updateComplete;
+ for (const select of selects) {
+ assert.isFalse(select.disabled);
+ }
+ });
+
+ test('remove rule and undo remove', async () => {
+ element.editing = true;
+ element.rule = {value: {action: 'ALLOW'}};
+ await element.updateComplete;
+ assert.isFalse(
+ queryAndAssert<HTMLDivElement>(
+ element,
+ '#deletedContainer'
+ ).classList.contains('deleted')
+ );
+ MockInteractions.tap(queryAndAssert<GrButton>(element, '#removeBtn'));
+ await element.updateComplete;
+ assert.isTrue(
+ queryAndAssert<HTMLDivElement>(
+ element,
+ '#deletedContainer'
+ ).classList.contains('deleted')
+ );
+ assert.isTrue(element.deleted);
+ assert.isTrue(element.rule!.value!.deleted);
+
+ MockInteractions.tap(queryAndAssert<GrButton>(element, '#undoRemoveBtn'));
+ await element.updateComplete;
+ assert.isFalse(element.deleted);
+ assert.isNotOk(element.rule!.value!.deleted);
+ });
+
+ test('remove rule and cancel', async () => {
+ element.editing = true;
+ await element.updateComplete;
+ assert.notEqual(
+ getComputedStyle(queryAndAssert<GrButton>(element, '#removeBtn'))
+ .display,
+ 'none'
+ );
+ assert.equal(
+ getComputedStyle(
+ queryAndAssert<HTMLDivElement>(element, '#deletedContainer')
+ ).display,
+ 'none'
+ );
+
+ element.rule = {value: {action: 'ALLOW'}};
+ await element.updateComplete;
+ MockInteractions.tap(queryAndAssert<GrButton>(element, '#removeBtn'));
+ await element.updateComplete;
+ assert.notEqual(
+ getComputedStyle(queryAndAssert<GrButton>(element, '#removeBtn'))
+ .display,
+ 'none'
+ );
+ assert.notEqual(
+ getComputedStyle(
+ queryAndAssert<HTMLDivElement>(element, '#deletedContainer')
+ ).display,
+ 'none'
+ );
+ assert.isTrue(element.deleted);
+ assert.isTrue(element.rule!.value!.deleted);
+
+ element.editing = false;
+ await element.updateComplete;
+ assert.isFalse(element.deleted);
+ assert.isNotOk(element.rule!.value!.deleted);
+ assert.isNotOk(element.rule!.value!.modified);
+
+ assert.deepEqual(element.originalRuleValues, element.rule!.value);
+ assert.equal(
+ getComputedStyle(queryAndAssert<GrButton>(element, '#removeBtn'))
+ .display,
+ 'none'
+ );
+ assert.equal(
+ getComputedStyle(
+ queryAndAssert<HTMLDivElement>(element, '#deletedContainer')
+ ).display,
+ 'none'
+ );
+ });
+
+ test('computeGroupPath', () => {
+ const group = '123';
+ assert.equal(element.computeGroupPath(group), '/admin/groups/123');
+ });
+ });
+
+ suite('new edit rule', () => {
+ setup(async () => {
+ element.groupName = 'Group Name';
+ element.permission = 'editTopicName' as AccessPermissionId;
+ element.rule = {};
+ element.section = 'refs/*';
+ element.setupValues();
+ await element.updateComplete;
+ element.rule!.value!.added = true;
+ await element.updateComplete;
+ element.connectedCallback();
+ });
+
+ test('_ruleValues and originalRuleValues are set correctly', () => {
+ // Since the element does not already have default values, they should
+ // be set. The original values should be set to those too.
+ assert.isNotOk(element.rule!.value!.modified);
+ const expectedRuleValue = {
+ action: 'ALLOW',
+ force: false,
+ added: true,
+ };
+ assert.deepEqual(element.rule!.value, expectedRuleValue);
+ test('values are set correctly', () => {
+ assert.equal(
+ queryAndAssert<GrSelect>(element, '#action').bindValue,
+ expectedRuleValue.action
+ );
+ assert.equal(
+ queryAndAssert<GrSelect>(element, '#force').bindValue,
+ expectedRuleValue.action
+ );
+ });
+ });
+
+ test('modify value', async () => {
+ assert.isNotOk(element.rule!.value!.modified);
+ const forceBindValue = queryAndAssert<GrSelect>(element, '#force');
+ forceBindValue.bindValue = 'true';
+ await element.updateComplete;
+ assert.isTrue(element.rule!.value!.modified);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element.originalRuleValues, element.rule!.value);
+ });
+
+ test('remove value', async () => {
+ element.editing = true;
+ const removeStub = sinon.stub();
+ element.addEventListener('added-rule-removed', removeStub);
+ MockInteractions.tap(queryAndAssert<GrButton>(element, '#removeBtn'));
+ await element.updateComplete;
+ assert.isTrue(removeStub.called);
+ });
+ });
+
+ suite('already existing rule with labels', () => {
+ setup(async () => {
+ element.label = {
+ values: [
+ {value: -2, text: 'This shall not be merged'},
+ {value: -1, text: 'I would prefer this is not merged as is'},
+ {value: -0, text: 'No score'},
+ {value: 1, text: 'Looks good to me, but someone else must approve'},
+ {value: 2, text: 'Looks good to me, approved'},
+ ],
+ };
+ element.groupName = 'Group Name';
+ element.permission = 'label-Code-Review' as AccessPermissionId;
+ element.rule = {
+ value: {
+ action: 'ALLOW',
+ force: false,
+ max: 2,
+ min: -2,
+ },
+ };
+ element.section = 'refs/*';
+ element.setupValues();
+ await element.updateComplete;
+ element.connectedCallback();
+ });
+
+ test('_ruleValues and originalRuleValues are set correctly', () => {
+ assert.deepEqual(element.originalRuleValues, element.rule!.value);
+ });
+
+ test('values are set correctly', () => {
+ assert.equal(
+ queryAndAssert<GrSelect>(element, '#action').bindValue,
+ element.rule!.value!.action
+ );
+ assert.equal(
+ queryAndAssert<GrSelect>(element, '#labelMin').bindValue,
+ element.rule!.value!.min
+ );
+ assert.equal(
+ queryAndAssert<GrSelect>(element, '#labelMax').bindValue,
+ element.rule!.value!.max
+ );
+ assert.isFalse(
+ queryAndAssert<GrSelect>(element, '#force').classList.contains('force')
+ );
+ });
+
+ test('modify value', async () => {
+ const removeStub = sinon.stub();
+ element.addEventListener('added-rule-removed', removeStub);
+ assert.isNotOk(element.rule!.value!.modified);
+ const labelMinBindValue = queryAndAssert<GrSelect>(element, '#labelMin');
+ labelMinBindValue.bindValue = 1;
+ await element.updateComplete;
+ assert.isTrue(element.rule!.value!.modified);
+ assert.isFalse(removeStub.called);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element.originalRuleValues, element.rule!.value);
+ });
+ });
+
+ suite('new rule with labels', () => {
+ let setDefaultRuleValuesSpy: sinon.SinonSpy;
+
+ setup(async () => {
+ setDefaultRuleValuesSpy = sinon.spy(element, 'setDefaultRuleValues');
+ element.label = {
+ values: [
+ {value: -2, text: 'This shall not be merged'},
+ {value: -1, text: 'I would prefer this is not merged as is'},
+ {value: -0, text: 'No score'},
+ {value: 1, text: 'Looks good to me, but someone else must approve'},
+ {value: 2, text: 'Looks good to me, approved'},
+ ],
+ };
+ element.groupName = 'Group Name';
+ element.permission = 'label-Code-Review' as AccessPermissionId;
+ element.rule = {};
+ element.section = 'refs/*';
+ element.setupValues();
+ await element.updateComplete;
+ element.rule!.value!.added = true;
+ await element.updateComplete;
+ element.connectedCallback();
+ });
+
+ test('_ruleValues and originalRuleValues are set correctly', () => {
+ // Since the element does not already have default values, they should
+ // be set. The original values should be set to those too.
+ assert.isNotOk(element.rule!.value!.modified);
+ assert.isTrue(setDefaultRuleValuesSpy.called);
+
+ const expectedRuleValue = {
+ max: element.label!.values![element.label!.values.length - 1].value,
+ min: element.label!.values![0].value,
+ action: 'ALLOW',
+ added: true,
+ };
+ assert.deepEqual(element.rule!.value, expectedRuleValue);
+ test('values are set correctly', () => {
+ assert.equal(
+ queryAndAssert<GrSelect>(element, '#action').bindValue,
+ expectedRuleValue.action
+ );
+ assert.equal(
+ queryAndAssert<GrSelect>(element, '#labelMin').bindValue,
+ expectedRuleValue.min
+ );
+ assert.equal(
+ queryAndAssert<GrSelect>(element, '#labelMax').bindValue,
+ expectedRuleValue.max
+ );
+ });
+ });
+
+ test('modify value', async () => {
+ assert.isNotOk(element.rule!.value!.modified);
+ const labelMinBindValue = queryAndAssert<GrSelect>(element, '#labelMin');
+ labelMinBindValue.bindValue = 1;
+ await element.updateComplete;
+ assert.isTrue(element.rule!.value!.modified);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element.originalRuleValues, element.rule!.value);
+ });
+ });
+
+ suite('already existing push rule', () => {
+ setup(async () => {
+ element.groupName = 'Group Name';
+ element.permission = 'push' as AccessPermissionId;
+ element.rule = {
+ value: {
+ action: 'ALLOW',
+ force: true,
+ },
+ };
+ element.section = 'refs/*';
+ element.setupValues();
+ await element.updateComplete;
+ element.connectedCallback();
+ });
+
+ test('_ruleValues and originalRuleValues are set correctly', () => {
+ assert.deepEqual(element.originalRuleValues, element.rule!.value);
+ });
+
+ test('values are set correctly', () => {
+ assert.isTrue(
+ queryAndAssert<GrSelect>(element, '#force').classList.contains('force')
+ );
+ assert.equal(
+ queryAndAssert<GrSelect>(element, '#action').bindValue,
+ element.rule!.value!.action
+ );
+ assert.equal(
+ queryAndAssert<GrSelect>(element, '#force').bindValue,
+ element.rule!.value!.force
+ );
+ assert.isNotOk(query<GrSelect>(element, '#labelMin'));
+ assert.isNotOk(query<GrSelect>(element, '#labelMax'));
+ });
+
+ test('modify value', async () => {
+ assert.isNotOk(element.rule!.value!.modified);
+ const actionBindValue = queryAndAssert<GrSelect>(element, '#action');
+ actionBindValue.bindValue = false;
+ await element.updateComplete;
+ assert.isTrue(element.rule!.value!.modified);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element.originalRuleValues, element.rule!.value);
+ });
+ });
+
+ suite('new push rule', async () => {
+ setup(async () => {
+ element.groupName = 'Group Name';
+ element.permission = 'push' as AccessPermissionId;
+ element.rule = {};
+ element.section = 'refs/*';
+ element.setupValues();
+ await element.updateComplete;
+ element.rule!.value!.added = true;
+ await element.updateComplete;
+ element.connectedCallback();
+ });
+
+ test('_ruleValues and originalRuleValues are set correctly', () => {
+ // Since the element does not already have default values, they should
+ // be set. The original values should be set to those too.
+ assert.isNotOk(element.rule!.value!.modified);
+ const expectedRuleValue = {
+ action: 'ALLOW',
+ force: false,
+ added: true,
+ };
+ assert.deepEqual(element.rule!.value, expectedRuleValue);
+ test('values are set correctly', () => {
+ assert.equal(
+ queryAndAssert<GrSelect>(element, '#action').bindValue,
+ expectedRuleValue.action
+ );
+ assert.equal(
+ queryAndAssert<GrSelect>(element, '#force').bindValue,
+ expectedRuleValue.action
+ );
+ });
+ });
+
+ test('modify value', async () => {
+ assert.isNotOk(element.rule!.value!.modified);
+ const forceBindValue = queryAndAssert<GrSelect>(element, '#force');
+ forceBindValue.bindValue = true;
+ await element.updateComplete;
+ assert.isTrue(element.rule!.value!.modified);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element.originalRuleValues, element.rule!.value);
+ });
+ });
+
+ suite('already existing edit rule', () => {
+ setup(async () => {
+ element.groupName = 'Group Name';
+ element.permission = 'editTopicName' as AccessPermissionId;
+ element.rule = {
+ value: {
+ action: 'ALLOW',
+ force: true,
+ },
+ };
+ element.section = 'refs/*';
+ element.setupValues();
+ await element.updateComplete;
+ element.connectedCallback();
+ });
+
+ test('_ruleValues and originalRuleValues are set correctly', () => {
+ assert.deepEqual(element.originalRuleValues, element.rule!.value);
+ });
+
+ test('values are set correctly', () => {
+ assert.isTrue(
+ queryAndAssert<GrSelect>(element, '#force').classList.contains('force')
+ );
+ assert.equal(
+ queryAndAssert<GrSelect>(element, '#action').bindValue,
+ element.rule!.value!.action
+ );
+ assert.equal(
+ queryAndAssert<GrSelect>(element, '#force').bindValue,
+ element.rule!.value!.force
+ );
+ assert.isNotOk(query<GrSelect>(element, '#labelMin'));
+ assert.isNotOk(query<GrSelect>(element, '#labelMax'));
+ });
+
+ test('modify value', async () => {
+ assert.isNotOk(element.rule!.value!.modified);
+ const actionBindValue = queryAndAssert<GrSelect>(element, '#action');
+ actionBindValue.bindValue = false;
+ await element.updateComplete;
+ assert.isTrue(element.rule!.value!.modified);
+
+ // The original value should now differ from the rule values.
+ assert.notDeepEqual(element.originalRuleValues, element.rule!.value);
+ });
+ });
+});
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-column/gr-change-list-column.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-column/gr-change-list-column.ts
new file mode 100644
index 0000000..4d16550
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-column/gr-change-list-column.ts
@@ -0,0 +1,157 @@
+/**
+ * @license
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../change/gr-submit-requirement-dashboard-hovercard/gr-submit-requirement-dashboard-hovercard';
+import '../../shared/gr-change-status/gr-change-status';
+import {LitElement, css, html, TemplateResult} from 'lit';
+import {customElement, property} from 'lit/decorators';
+import {ChangeInfo, SubmitRequirementStatus} from '../../../api/rest-api';
+import {changeStatuses} from '../../../utils/change-util';
+import {getRequirements, iconForStatus} from '../../../utils/label-util';
+import {submitRequirementsStyles} from '../../../styles/gr-submit-requirements-styles';
+import {pluralize} from '../../../utils/string-util';
+
+@customElement('gr-change-list-column-requirements')
+export class GrChangeListColumRequirements extends LitElement {
+ @property({type: Object})
+ change?: ChangeInfo;
+
+ static override get styles() {
+ return [
+ submitRequirementsStyles,
+ css`
+ iron-icon {
+ width: var(--line-height-normal, 20px);
+ height: var(--line-height-normal, 20px);
+ vertical-align: top;
+ }
+ iron-icon.block,
+ iron-icon.check-circle-filled {
+ margin-right: var(--spacing-xs);
+ }
+ iron-icon.commentIcon {
+ color: var(--deemphasized-text-color);
+ margin-left: var(--spacing-s);
+ }
+ span {
+ line-height: var(--line-height-normal);
+ }
+ span.check-circle-filled {
+ color: var(--success-foreground);
+ }
+ .unsatisfied {
+ color: var(--primary-text-color);
+ }
+ .total {
+ margin-left: var(--spacing-xs);
+ color: var(--deemphasized-text-color);
+ }
+ :host {
+ align-items: center;
+ display: inline-flex;
+ }
+ .comma {
+ padding-right: var(--spacing-xs);
+ }
+ /* Used to hide the leading separator comma for statuses. */
+ .comma:first-of-type {
+ display: none;
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ const commentIcon = this.renderCommentIcon();
+ return html`${this.renderChangeStatus()} ${commentIcon}`;
+ }
+
+ renderChangeStatus() {
+ if (!this.change) return;
+ const statuses = changeStatuses(this.change);
+ if (statuses.length > 0) {
+ return statuses.map(
+ status => html`
+ <div class="comma">,</div>
+ <gr-change-status flat .status=${status}></gr-change-status>
+ `
+ );
+ }
+ return this.renderActiveStatus();
+ }
+
+ renderActiveStatus() {
+ const submitRequirements = getRequirements(this.change);
+ if (!submitRequirements.length) return html`n/a`;
+ const numRequirements = submitRequirements.length;
+ const numSatisfied = submitRequirements.filter(
+ req =>
+ req.status === SubmitRequirementStatus.SATISFIED ||
+ req.status === SubmitRequirementStatus.OVERRIDDEN
+ ).length;
+
+ if (numSatisfied === numRequirements) {
+ return this.renderState(
+ iconForStatus(SubmitRequirementStatus.SATISFIED),
+ 'Ready'
+ );
+ }
+
+ const numUnsatisfied = submitRequirements.filter(
+ req => req.status === SubmitRequirementStatus.UNSATISFIED
+ ).length;
+
+ return this.renderState(
+ iconForStatus(SubmitRequirementStatus.UNSATISFIED),
+ this.renderSummary(numUnsatisfied, numRequirements)
+ );
+ }
+
+ renderState(icon: string, aggregation: string | TemplateResult) {
+ return html`<span class="${icon}"
+ ><gr-submit-requirement-dashboard-hovercard .change=${this.change}>
+ </gr-submit-requirement-dashboard-hovercard>
+ <iron-icon class="${icon}" icon="gr-icons:${icon}" role="img"></iron-icon
+ >${aggregation}</span
+ >`;
+ }
+
+ renderSummary(numUnsatisfied: number, numRequirements: number) {
+ return html`<span
+ ><span class="unsatisfied">${numUnsatisfied}</span
+ ><span class="total">(of ${numRequirements})</span></span
+ >`;
+ }
+
+ renderCommentIcon() {
+ if (!this.change?.unresolved_comment_count) return;
+ return html`<iron-icon
+ icon="gr-icons:comment"
+ class="commentIcon"
+ .title="${pluralize(
+ this.change?.unresolved_comment_count,
+ 'unresolved comment'
+ )}"
+ ></iron-icon>`;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-change-list-column-requirements': GrChangeListColumRequirements;
+ }
+}
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 c476d2d..12df6aa 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
@@ -15,7 +15,6 @@
* limitations under the License.
*/
-import '../../../styles/gr-change-list-styles';
import '../../shared/gr-account-link/gr-account-link';
import '../../shared/gr-change-star/gr-change-star';
import '../../shared/gr-change-status/gr-change-status';
@@ -23,20 +22,18 @@
import '../../shared/gr-icons/gr-icons';
import '../../shared/gr-limited-text/gr-limited-text';
import '../../shared/gr-tooltip-content/gr-tooltip-content';
-import '../../../styles/shared-styles';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-change-list-item_html';
+import '../gr-change-list-column/gr-change-list-column';
+import '../../shared/gr-tooltip-content/gr-tooltip-content';
import {GerritNav} 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 {appContext} from '../../../services/app-context';
+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 {customElement, property} from '@polymer/decorators';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
import {
ChangeInfo,
@@ -47,7 +44,17 @@
} from '../../../types/common';
import {hasOwnProperty} from '../../../utils/common-util';
import {pluralize} from '../../../utils/string-util';
-import {ChangeStates} from '../../shared/gr-change-status/gr-change-status';
+import {
+ getRequirements,
+ iconForStatus,
+ showNewSubmitRequirements,
+} from '../../../utils/label-util';
+import {changeListStyles} from '../../../styles/gr-change-list-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, css, html} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+import {submitRequirementsStyles} from '../../../styles/gr-submit-requirements-styles';
+import {ifDefined} from 'lit/directives/if-defined';
enum ChangeSize {
XS = 10,
@@ -67,20 +74,19 @@
REJECTED = 'REJECTED',
}
-export interface ChangeListToggleReviewedDetail {
- change: ChangeInfo;
- reviewed: boolean;
-}
-
// How many reviewers should be shown with an account-label?
const PRIMARY_REVIEWERS_COUNT = 2;
-@customElement('gr-change-list-item')
-export class GrChangeListItem extends PolymerElement {
- static get template() {
- return htmlTemplate;
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-change-list-item': GrChangeListItem;
}
-
+}
+/**
+ * @attr {Boolean} selected - change list item is selected by cursor
+ */
+@customElement('gr-change-list-item')
+export class GrChangeListItem extends LitElement {
/** The logged-in user's account, or null if no user is logged in. */
@property({type: Object})
account: AccountInfo | null = null;
@@ -101,56 +107,501 @@
@property({type: String})
sectionName?: string;
- @property({type: String, computed: '_computeChangeURL(change)'})
- changeURL?: string;
-
- @property({type: Array, computed: '_changeStatuses(change)'})
- statuses?: ChangeStates[];
-
@property({type: Boolean})
showStar = false;
@property({type: Boolean})
showNumber = false;
- @property({type: String, computed: '_computeChangeSize(change)'})
- _changeSize?: string;
+ @state() private dynamicCellEndpoints?: string[];
- @property({type: Array})
- _dynamicCellEndpoints?: string[];
+ reporting: ReportingService = getAppContext().reportingService;
- reporting: ReportingService = appContext.reportingService;
+ private readonly flagsService = getAppContext().flagsService;
override connectedCallback() {
super.connectedCallback();
getPluginLoader()
.awaitPluginsLoaded()
.then(() => {
- this._dynamicCellEndpoints = getPluginEndpoints().getDynamicEndpoints(
+ this.dynamicCellEndpoints = getPluginEndpoints().getDynamicEndpoints(
'change-list-item-cell'
);
});
}
- _changeStatuses(change?: ChangeInfo) {
- if (!change) return [];
- return changeStatuses(change);
+ static override get styles() {
+ return [
+ changeListStyles,
+ sharedStyles,
+ submitRequirementsStyles,
+ css`
+ :host {
+ display: table-row;
+ color: var(--primary-text-color);
+ }
+ :host(:focus) {
+ outline: none;
+ }
+ :host(:hover) {
+ background-color: var(--hover-background-color);
+ }
+ .container {
+ position: relative;
+ }
+ .content {
+ overflow: hidden;
+ position: absolute;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 100%;
+ }
+ .content a {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 100%;
+ }
+ .comments,
+ .reviewers,
+ .requirements {
+ white-space: nowrap;
+ }
+ .reviewers {
+ --account-max-length: 70px;
+ }
+ .spacer {
+ height: 0;
+ overflow: hidden;
+ }
+ .status {
+ align-items: center;
+ display: inline-flex;
+ }
+ .status .comma {
+ padding-right: var(--spacing-xs);
+ }
+ /* Used to hide the leading separator comma for statuses. */
+ .status .comma:first-of-type {
+ display: none;
+ }
+ .size gr-tooltip-content {
+ margin: -0.4rem -0.6rem;
+ max-width: 2.5rem;
+ padding: var(--spacing-m) var(--spacing-l);
+ }
+ .size span {
+ border-radius: var(--border-radius);
+ color: var(--dashboard-size-text);
+ font-size: var(--font-size-small);
+ /* To set height and width of span, it has to be inline block */
+ display: inline-block;
+ height: 20px;
+ width: 20px;
+ text-align: center;
+ vertical-align: top;
+ }
+ .size span.size-xs {
+ background-color: var(--dashboard-size-xs);
+ color: var(--dashboard-size-xs-text);
+ }
+ .size span.size-s {
+ background-color: var(--dashboard-size-s);
+ }
+ .size span.size-m {
+ background-color: var(--dashboard-size-m);
+ }
+ .size span.size-l {
+ background-color: var(--dashboard-size-l);
+ }
+ .size span.size-xl {
+ background-color: var(--dashboard-size-xl);
+ color: var(--dashboard-size-xl-text);
+ }
+ a {
+ color: inherit;
+ cursor: pointer;
+ text-decoration: none;
+ }
+ a:hover {
+ text-decoration: underline;
+ }
+ .subject:hover .content {
+ text-decoration: underline;
+ }
+ .u-monospace {
+ font-family: var(--monospace-font-family);
+ font-size: var(--font-size-mono);
+ line-height: var(--line-height-mono);
+ }
+ .u-green,
+ .u-green iron-icon {
+ color: var(--positive-green-text-color);
+ }
+ .u-red,
+ .u-red iron-icon {
+ color: var(--negative-red-text-color);
+ }
+ .u-gray-background {
+ background-color: var(--table-header-background-color);
+ }
+ .comma,
+ .placeholder {
+ color: var(--deemphasized-text-color);
+ }
+ .cell.label {
+ font-weight: var(--font-weight-normal);
+ }
+ .cell.label iron-icon {
+ vertical-align: top;
+ }
+ @media only screen and (max-width: 50em) {
+ :host {
+ display: flex;
+ }
+ }
+ `,
+ ];
}
- _computeChangeURL(change?: ChangeInfo) {
- if (!change) return '';
- return GerritNav.getUrlForChange(change);
+ override render() {
+ const changeUrl = this.computeChangeURL();
+ return html`
+ <td aria-hidden="true" class="cell leftPadding"></td>
+ ${this.renderCellStar()} ${this.renderCellNumber(changeUrl)}
+ ${this.renderCellSubject(changeUrl)} ${this.renderCellStatus()}
+ ${this.renderCellOwner()} ${this.renderCellReviewers()}
+ ${this.renderCellComments()} ${this.renderCellRepo()}
+ ${this.renderCellBranch()} ${this.renderCellUpdated()}
+ ${this.renderCellSubmitted()} ${this.renderCellWaiting()}
+ ${this.renderCellSize()} ${this.renderCellRequirements()}
+ ${this.labelNames?.map(labelNames => this.renderChangeLabels(labelNames))}
+ ${this.dynamicCellEndpoints?.map(pluginEndpointName =>
+ this.renderChangePluginEndpoint(pluginEndpointName)
+ )}
+ `;
}
- _computeLabelTitle(change: ChangeInfo | undefined, labelName: string) {
- const label: QuickLabelInfo | undefined = change?.labels?.[labelName];
- const category = this._computeLabelCategory(change, labelName);
+ private renderCellStar() {
+ if (!this.showStar) return;
+
+ return html`
+ <td class="cell star">
+ <gr-change-star .change=${this.change}></gr-change-star>
+ </td>
+ `;
+ }
+
+ private renderCellNumber(changeUrl: string) {
+ if (!this.showNumber) return;
+
+ return html`
+ <td class="cell number">
+ <a href="${changeUrl}">${this.change?._number}</a>
+ </td>
+ `;
+ }
+
+ private renderCellSubject(changeUrl: string) {
+ if (this.computeIsColumnHidden('Subject', this.visibleChangeTableColumns))
+ return;
+
+ return html`
+ <td class="cell subject">
+ <a
+ title="${ifDefined(this.change?.subject)}"
+ href="${changeUrl}"
+ @click=${() => this.handleChangeClick()}
+ >
+ <div class="container">
+ <div class="content">${this.change?.subject}</div>
+ <div class="spacer">${this.change?.subject}</div>
+ <span> </span>
+ </div>
+ </a>
+ </td>
+ `;
+ }
+
+ private renderCellStatus() {
+ if (this.computeIsColumnHidden('Status', this.visibleChangeTableColumns))
+ return;
+
+ return html` <td class="cell status">${this.renderChangeStatus()}</td> `;
+ }
+
+ private renderChangeStatus() {
+ if (!this.changeStatuses().length) {
+ return html`<span class="placeholder">--</span>`;
+ }
+
+ return this.changeStatuses().map(
+ status => html`
+ <div class="comma">,</div>
+ <gr-change-status flat .status=${status}></gr-change-status>
+ `
+ );
+ }
+
+ private renderCellOwner() {
+ if (this.computeIsColumnHidden('Owner', this.visibleChangeTableColumns))
+ return;
+
+ return html`
+ <td class="cell owner">
+ <gr-account-link
+ highlightAttention
+ .change=${this.change}
+ .account=${this.change?.owner}
+ ></gr-account-link>
+ </td>
+ `;
+ }
+
+ private renderCellReviewers() {
+ if (this.computeIsColumnHidden('Reviewers', this.visibleChangeTableColumns))
+ return;
+
+ return html`
+ <td class="cell reviewers">
+ <div>
+ ${this.computePrimaryReviewers().map((reviewer, index) =>
+ this.renderChangeReviewers(reviewer, index)
+ )}
+ ${this.computeAdditionalReviewersCount()
+ ? html`<span title="${this.computeAdditionalReviewersTitle()}"
+ >+${this.computeAdditionalReviewersCount()}</span
+ >`
+ : ''}
+ </div>
+ </td>
+ `;
+ }
+
+ private renderChangeReviewers(reviewer: AccountInfo, index: number) {
+ return html`
+ <gr-account-link
+ hideAvatar
+ hideStatus
+ firstName
+ highlightAttention
+ .change=${this.change}
+ .account=${reviewer}
+ ></gr-account-link
+ ><span ?hidden=${this.computeCommaHidden(index)} aria-hidden="true"
+ >,
+ </span>
+ `;
+ }
+
+ private renderCellComments() {
+ if (this.computeIsColumnHidden('Comments', this.visibleChangeTableColumns))
+ return;
+
+ return html`
+ <td class="cell comments">
+ ${this.change?.unresolved_comment_count
+ ? html`<iron-icon icon="gr-icons:comment"></iron-icon>`
+ : ''}
+ <span
+ >${this.computeComments(this.change?.unresolved_comment_count)}</span
+ >
+ </td>
+ `;
+ }
+
+ private renderCellRepo() {
+ if (this.computeIsColumnHidden('Repo', this.visibleChangeTableColumns))
+ return;
+
+ return html`
+ <td class="cell repo">
+ <a class="fullRepo" href="${this.computeRepoUrl()}">
+ ${this.computeRepoDisplay()}
+ </a>
+ <a
+ class="truncatedRepo"
+ href="${this.computeRepoUrl()}"
+ title="${this.computeRepoDisplay()}"
+ >
+ ${this.computeTruncatedRepoDisplay()}
+ </a>
+ </td>
+ `;
+ }
+
+ private renderCellBranch() {
+ if (this.computeIsColumnHidden('Branch', this.visibleChangeTableColumns))
+ return;
+
+ return html`
+ <td class="cell branch">
+ <a href="${this.computeRepoBranchURL()}"> ${this.change?.branch} </a>
+ ${this.renderChangeBranch()}
+ </td>
+ `;
+ }
+
+ private renderChangeBranch() {
+ if (!this.change?.topic) return;
+
+ return html`
+ (<a href="${this.computeTopicURL()}"
+ ><!--
+ --><gr-limited-text .limit=${50} .text=${this.change.topic}>
+ </gr-limited-text
+ ><!--
+ --></a
+ >)
+ `;
+ }
+
+ private renderCellUpdated() {
+ if (this.computeIsColumnHidden('Updated', this.visibleChangeTableColumns))
+ return;
+
+ return html`
+ <td class="cell updated">
+ <gr-date-formatter
+ withTooltip
+ .dateStr=${this.formatDate(this.change?.updated)}
+ ></gr-date-formatter>
+ </td>
+ `;
+ }
+
+ private renderCellSubmitted() {
+ if (this.computeIsColumnHidden('Submitted', this.visibleChangeTableColumns))
+ return;
+
+ return html`
+ <td class="cell submitted">
+ <gr-date-formatter
+ withTooltip
+ .dateStr=${this.formatDate(this.change?.submitted)}
+ ></gr-date-formatter>
+ </td>
+ `;
+ }
+
+ private renderCellWaiting() {
+ if (this.computeIsColumnHidden('Waiting', this.visibleChangeTableColumns))
+ return;
+
+ return html`
+ <td class="cell waiting">
+ <gr-date-formatter
+ withTooltip
+ forceRelative
+ relativeOptionNoAgo
+ .dateStr=${this.computeWaiting()}
+ ></gr-date-formatter>
+ </td>
+ `;
+ }
+
+ private renderCellSize() {
+ if (this.computeIsColumnHidden('Size', this.visibleChangeTableColumns))
+ return;
+
+ return html`
+ <td class="cell size">
+ <gr-tooltip-content has-tooltip title="${this.computeSizeTooltip()}">
+ ${this.renderChangeSize()}
+ </gr-tooltip-content>
+ </td>
+ `;
+ }
+
+ private renderChangeSize() {
+ const changeSize = this.computeChangeSize();
+ if (!changeSize) return html`<span class="placeholder">--</span>`;
+
+ return html`
+ <span class="size-${changeSize.toLowerCase()}">${changeSize}</span>
+ `;
+ }
+
+ private renderCellRequirements() {
+ if (this.computeIsColumnHidden(' Status ', this.visibleChangeTableColumns))
+ return;
+
+ return html`
+ <td class="cell requirements">
+ <gr-change-list-column-requirements .change=${this.change}>
+ </gr-change-list-column-requirements>
+ </td>
+ `;
+ }
+
+ private renderChangeLabels(labelName: string) {
+ return html`
+ <td
+ title="${this.computeLabelTitle(labelName)}"
+ class="${this.computeLabelClass(labelName)}"
+ >
+ ${this.renderChangeHasLabelIcon(labelName)}
+ </td>
+ `;
+ }
+
+ private renderChangeHasLabelIcon(labelName: string) {
+ if (showNewSubmitRequirements(this.flagsService, this.change)) {
+ let requirements = getRequirements(this.change).filter(
+ sr => sr.name === labelName
+ );
+ // TODO(milutin): Remove this after migration from legacy requirements.
+ if (requirements.length > 1) {
+ requirements = requirements.filter(sr => !sr.is_legacy);
+ }
+ if (requirements.length === 1) {
+ const icon = iconForStatus(requirements[0].status);
+ return html`<iron-icon
+ class="${icon}"
+ icon="gr-icons:${icon}"
+ ></iron-icon>`;
+ }
+ }
+ if (this.computeLabelIcon(labelName) === '')
+ return html`<span>${this.computeLabelValue(labelName)}</span>`;
+
+ return html`
+ <iron-icon icon=${this.computeLabelIcon(labelName)}></iron-icon>
+ `;
+ }
+
+ private renderChangePluginEndpoint(pluginEndpointName: string) {
+ return html`
+ <td class="cell endpoint">
+ <gr-endpoint-decorator name="${pluginEndpointName}">
+ <gr-endpoint-param name="change" .value=${this.change}>
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </td>
+ `;
+ }
+
+ private changeStatuses() {
+ if (!this.change) return [];
+ return changeStatuses(this.change);
+ }
+
+ private computeChangeURL() {
+ if (!this.change) return '';
+ return GerritNav.getUrlForChange(this.change);
+ }
+
+ // private but used in test
+ computeLabelTitle(labelName: string) {
+ const label: QuickLabelInfo | undefined = this.change?.labels?.[labelName];
+ const category = this.computeLabelCategory(labelName);
if (!label || category === LabelCategory.NOT_APPLICABLE) {
return 'Label not applicable';
}
const titleParts: string[] = [];
if (category === LabelCategory.UNRESOLVED_COMMENTS) {
- const num = change?.unresolved_comment_count ?? 0;
+ const num = this.change?.unresolved_comment_count ?? 0;
titleParts.push(pluralize(num, 'unresolved comment'));
}
const significantLabel =
@@ -164,9 +615,20 @@
return labelName;
}
- _computeLabelClass(change: ChangeInfo | undefined, labelName: string) {
- const category = this._computeLabelCategory(change, labelName);
+ // private but used in test
+ computeLabelClass(labelName: string) {
const classes = ['cell', 'label'];
+ if (showNewSubmitRequirements(this.flagsService, this.change)) {
+ const requirements = getRequirements(this.change).filter(
+ sr => sr.name === labelName
+ );
+ if (requirements.length === 1) {
+ classes.push('requirement');
+ // Do not add label category classes.
+ return classes.sort().join(' ');
+ }
+ }
+ const category = this.computeLabelCategory(labelName);
switch (category) {
case LabelCategory.NOT_APPLICABLE:
classes.push('u-gray-background');
@@ -189,12 +651,9 @@
return classes.sort().join(' ');
}
- _computeHasLabelIcon(change: ChangeInfo | undefined, labelName: string) {
- return this._computeLabelIcon(change, labelName) !== '';
- }
-
- _computeLabelIcon(change: ChangeInfo | undefined, labelName: string): string {
- const category = this._computeLabelCategory(change, labelName);
+ // private but used in test
+ computeLabelIcon(labelName: string): string {
+ const category = this.computeLabelCategory(labelName);
switch (category) {
case LabelCategory.APPROVED:
return 'gr-icons:check';
@@ -207,8 +666,9 @@
}
}
- _computeLabelCategory(change: ChangeInfo | undefined, labelName: string) {
- const label: QuickLabelInfo | undefined = change?.labels?.[labelName];
+ // private but used in test
+ computeLabelCategory(labelName: string) {
+ const label: QuickLabelInfo | undefined = this.change?.labels?.[labelName];
if (!label) {
return LabelCategory.NOT_APPLICABLE;
}
@@ -218,7 +678,7 @@
if (label.value && label.value < 0) {
return LabelCategory.NEGATIVE;
}
- if (change?.unresolved_comment_count && labelName === 'Code-Review') {
+ if (this.change?.unresolved_comment_count && labelName === 'Code-Review') {
return LabelCategory.UNRESOLVED_COMMENTS;
}
if (label.approved) {
@@ -230,9 +690,10 @@
return LabelCategory.NEUTRAL;
}
- _computeLabelValue(change: ChangeInfo | undefined, labelName: string) {
- const label: QuickLabelInfo | undefined = change?.labels?.[labelName];
- const category = this._computeLabelCategory(change, labelName);
+ // private but used in test
+ computeLabelValue(labelName: string) {
+ const label: QuickLabelInfo | undefined = this.change?.labels?.[labelName];
+ const category = this.computeLabelCategory(labelName);
switch (category) {
case LabelCategory.NOT_APPLICABLE:
return '';
@@ -248,33 +709,36 @@
return `${label?.value}`;
case LabelCategory.REJECTED:
return '\u2715'; // ✕
+ default:
+ return '';
}
}
- _computeRepoUrl(change?: ChangeInfo) {
- if (!change) return '';
+ private computeRepoUrl() {
+ if (!this.change) return '';
return GerritNav.getUrlForProjectChanges(
- change.project,
+ this.change.project,
true,
- change.internalHost
+ this.change.internalHost
);
}
- _computeRepoBranchURL(change?: ChangeInfo) {
- if (!change) return '';
+ private computeRepoBranchURL() {
+ if (!this.change) return '';
return GerritNav.getUrlForBranch(
- change.branch,
- change.project,
+ this.change.branch,
+ this.change.project,
undefined,
- change.internalHost
+ this.change.internalHost
);
}
- _computeTopicURL(change?: ChangeInfo) {
- if (!change?.topic) {
- return '';
- }
- return GerritNav.getUrlForTopic(change.topic, change.internalHost);
+ private computeTopicURL() {
+ if (!this.change?.topic) return '';
+ return GerritNav.getUrlForTopic(
+ this.change.topic,
+ this.change.internalHost
+ );
}
/**
@@ -283,44 +747,46 @@
*
* @param truncate whether or not the project name should be
* truncated. If this value is truthy, the name will be truncated.
+ *
+ * private but used in test
*/
- _computeRepoDisplay(change?: ChangeInfo) {
- if (!change?.project) {
- return '';
- }
+ computeRepoDisplay() {
+ if (!this.change?.project) return '';
let str = '';
- if (change.internalHost) {
- str += change.internalHost + '/';
+ if (this.change.internalHost) {
+ str += this.change.internalHost + '/';
}
- str += change.project;
+ str += this.change.project;
return str;
}
- _computeTruncatedRepoDisplay(change?: ChangeInfo) {
- if (!change?.project) {
+ // private but used in test
+ computeTruncatedRepoDisplay() {
+ if (!this.change?.project) {
return '';
}
let str = '';
- if (change.internalHost) {
- str += change.internalHost + '/';
+ if (this.change.internalHost) {
+ str += this.change.internalHost + '/';
}
- str += truncatePath(change.project, 2);
+ str += truncatePath(this.change.project, 2);
return str;
}
- _computeSizeTooltip(change?: ChangeInfo) {
+ // private but used in test
+ computeSizeTooltip() {
if (
- !change ||
- change.insertions + change.deletions === 0 ||
- isNaN(change.insertions + change.deletions)
+ !this.change ||
+ this.change.insertions + this.change.deletions === 0 ||
+ isNaN(this.change.insertions + this.change.deletions)
) {
return 'Size unknown';
} else {
- return `added ${change.insertions}, removed ${change.deletions} lines`;
+ return `added ${this.change.insertions}, removed ${this.change.deletions} lines`;
}
}
- _hasAttention(account: AccountInfo) {
+ private hasAttention(account: AccountInfo) {
if (!this.change || !this.change.attention_set || !account._account_id) {
return false;
}
@@ -330,12 +796,15 @@
/**
* Computes the array of all reviewers with sorting the reviewers in the
* attention set before others, and the current user first.
+ *
+ * private but used in test
*/
- _computeReviewers(change?: ChangeInfo) {
- if (!change?.reviewers || !change?.reviewers.REVIEWER) return [];
- const reviewers = [...change.reviewers.REVIEWER].filter(
+ computeReviewers() {
+ if (!this.change?.reviewers || !this.change?.reviewers.REVIEWER) return [];
+ const reviewers = [...this.change.reviewers.REVIEWER].filter(
r =>
- (!change.owner || change.owner._account_id !== r._account_id) &&
+ (!this.change?.owner ||
+ this.change?.owner._account_id !== r._account_id) &&
!isServiceUser(r)
);
reviewers.sort((r1, r2) => {
@@ -343,33 +812,33 @@
if (isSelf(r1, this.account)) return -1;
if (isSelf(r2, this.account)) return 1;
}
- if (this._hasAttention(r1) && !this._hasAttention(r2)) return -1;
- if (this._hasAttention(r2) && !this._hasAttention(r1)) return 1;
+ if (this.hasAttention(r1) && !this.hasAttention(r2)) return -1;
+ if (this.hasAttention(r2) && !this.hasAttention(r1)) return 1;
return (r1.name || '').localeCompare(r2.name || '');
});
return reviewers;
}
- _computePrimaryReviewers(change?: ChangeInfo) {
- return this._computeReviewers(change).slice(0, PRIMARY_REVIEWERS_COUNT);
+ private computePrimaryReviewers() {
+ return this.computeReviewers().slice(0, PRIMARY_REVIEWERS_COUNT);
}
- _computeAdditionalReviewers(change?: ChangeInfo) {
- return this._computeReviewers(change).slice(PRIMARY_REVIEWERS_COUNT);
+ private computeAdditionalReviewers() {
+ return this.computeReviewers().slice(PRIMARY_REVIEWERS_COUNT);
}
- _computeAdditionalReviewersCount(change?: ChangeInfo) {
- return this._computeAdditionalReviewers(change).length;
+ private computeAdditionalReviewersCount() {
+ return this.computeAdditionalReviewers().length;
}
- _computeAdditionalReviewersTitle(change?: ChangeInfo, config?: ServerInfo) {
- if (!change || !config) return '';
- return this._computeAdditionalReviewers(change)
- .map(user => getDisplayName(config, user, true))
+ private computeAdditionalReviewersTitle() {
+ if (!this.change || !this.config) return '';
+ return this.computeAdditionalReviewers()
+ .map(user => getDisplayName(this.config, user, true))
.join(', ');
}
- _computeComments(unresolved_comment_count?: number) {
+ private computeComments(unresolved_comment_count?: number) {
if (!unresolved_comment_count || unresolved_comment_count < 1) return '';
return `${unresolved_comment_count} unresolved`;
}
@@ -377,10 +846,12 @@
/**
* TShirt sizing is based on the following paper:
* http://dirkriehle.com/wp-content/uploads/2008/09/hicss-42-csdistr-final-web.pdf
+ *
+ * private but used in test
*/
- _computeChangeSize(change?: ChangeInfo) {
- if (!change) return null;
- const delta = change.insertions + change.deletions;
+ computeChangeSize() {
+ if (!this.change) return null;
+ const delta = this.change.insertions + this.change.deletions;
if (isNaN(delta) || delta === 0) {
return null; // Unknown
}
@@ -397,44 +868,28 @@
}
}
- _computeWaiting(
- account?: AccountInfo | null,
- change?: ChangeInfo | null
- ): Timestamp | undefined {
- if (!account?._account_id || !change?.attention_set) return undefined;
- return change?.attention_set[account._account_id]?.last_update;
+ private computeWaiting(): Timestamp | undefined {
+ if (!this.account?._account_id || !this.change?.attention_set)
+ return undefined;
+ return this.change?.attention_set[this.account._account_id]?.last_update;
}
- _computeIsColumnHidden(columnToCheck?: string, columnsToDisplay?: string[]) {
+ private computeIsColumnHidden(
+ columnToCheck?: string,
+ columnsToDisplay?: string[]
+ ) {
if (!columnsToDisplay || !columnToCheck) {
return false;
}
return !columnsToDisplay.includes(columnToCheck);
}
- toggleReviewed() {
- if (!this.change) return;
- const newVal = !this.change?.reviewed;
- this.set('change.reviewed', newVal);
- const detail: ChangeListToggleReviewedDetail = {
- change: this.change,
- reviewed: newVal,
- };
- this.dispatchEvent(
- new CustomEvent('toggle-reviewed', {
- bubbles: true,
- composed: true,
- detail,
- })
- );
- }
-
- _formatDate(date: Timestamp | undefined): string | undefined {
+ private formatDate(date: Timestamp | undefined): string | undefined {
if (!date) return undefined;
return date.toString();
}
- _handleChangeClick() {
+ private handleChangeClick() {
// Don't prevent the default and neither stop bubbling. We just want to
// report the click, but then let the browser handle the click on the link.
@@ -448,19 +903,10 @@
});
}
- _computeCommaHidden(index?: number, change?: ChangeInfo) {
- if (index === undefined) return false;
- if (change === undefined) return false;
-
- const additionalCount = this._computeAdditionalReviewersCount(change);
- const primaryCount = this._computePrimaryReviewers(change).length;
+ private computeCommaHidden(index: number) {
+ const additionalCount = this.computeAdditionalReviewersCount();
+ const primaryCount = this.computePrimaryReviewers().length;
const isLast = index === primaryCount - 1;
return isLast && additionalCount === 0;
}
}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-change-list-item': GrChangeListItem;
- }
-}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
deleted file mode 100644
index a0aa962..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
+++ /dev/null
@@ -1,318 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- display: table-row;
- color: var(--primary-text-color);
- }
- :host(:focus) {
- outline: none;
- }
- :host(:hover) {
- background-color: var(--hover-background-color);
- }
- .container {
- position: relative;
- }
- .content {
- overflow: hidden;
- position: absolute;
- text-overflow: ellipsis;
- white-space: nowrap;
- width: 100%;
- }
- .content a {
- display: block;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- width: 100%;
- }
- .comments,
- .reviewers {
- white-space: nowrap;
- }
- .reviewers {
- --account-max-length: 70px;
- }
- .spacer {
- height: 0;
- overflow: hidden;
- }
- .status {
- align-items: center;
- display: inline-flex;
- }
- .status .comma {
- padding-right: var(--spacing-xs);
- }
- /* Used to hide the leading separator comma for statuses. */
- .status .comma:first-of-type {
- display: none;
- }
- .size gr-tooltip-content {
- margin: -0.4rem -0.6rem;
- max-width: 2.5rem;
- padding: var(--spacing-m) var(--spacing-l);
- }
- a {
- color: inherit;
- cursor: pointer;
- text-decoration: none;
- }
- a:hover {
- text-decoration: underline;
- }
- .subject:hover .content {
- text-decoration: underline;
- }
- .u-monospace {
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-mono);
- line-height: var(--line-height-mono);
- }
- .u-green,
- .u-green iron-icon {
- color: var(--positive-green-text-color);
- }
- .u-red,
- .u-red iron-icon {
- color: var(--negative-red-text-color);
- }
- .u-gray-background {
- background-color: var(--table-header-background-color);
- }
- .comma,
- .placeholder {
- color: var(--deemphasized-text-color);
- }
- .cell.label {
- font-weight: var(--font-weight-normal);
- }
- .cell.label iron-icon {
- vertical-align: top;
- }
- @media only screen and (max-width: 50em) {
- :host {
- display: flex;
- }
- }
- </style>
- <style include="gr-change-list-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <td aria-hidden="true" class="cell leftPadding"></td>
- <td class="cell star" hidden$="[[!showStar]]" hidden="">
- <gr-change-star change="{{change}}"></gr-change-star>
- </td>
- <td class="cell number" hidden$="[[!showNumber]]" hidden="">
- <a href$="[[changeURL]]">[[change._number]]</a>
- </td>
- <td
- class="cell subject"
- hidden$="[[_computeIsColumnHidden('Subject', visibleChangeTableColumns)]]"
- >
- <a
- title$="[[change.subject]]"
- href$="[[changeURL]]"
- on-click="_handleChangeClick"
- >
- <div class="container">
- <div class="content">[[change.subject]]</div>
- <div class="spacer">[[change.subject]]</div>
- <span> </span>
- </div>
- </a>
- </td>
- <td
- class="cell status"
- hidden$="[[_computeIsColumnHidden('Status', visibleChangeTableColumns)]]"
- >
- <template is="dom-repeat" items="[[statuses]]" as="status">
- <div class="comma">,</div>
- <gr-change-status flat="" status="[[status]]"></gr-change-status>
- </template>
- <template is="dom-if" if="[[!statuses.length]]">
- <span class="placeholder">--</span>
- </template>
- </td>
- <td
- class="cell owner"
- hidden$="[[_computeIsColumnHidden('Owner', visibleChangeTableColumns)]]"
- >
- <gr-account-link
- highlightAttention
- change="[[change]]"
- account="[[change.owner]]"
- ></gr-account-link>
- </td>
- <td
- class="cell assignee"
- hidden$="[[_computeIsColumnHidden('Assignee', visibleChangeTableColumns)]]"
- >
- <template is="dom-if" if="[[change.assignee]]">
- <gr-account-link
- id="assigneeAccountLink"
- account="[[change.assignee]]"
- ></gr-account-link>
- </template>
- <template is="dom-if" if="[[!change.assignee]]">
- <span class="placeholder">--</span>
- </template>
- </td>
- <td
- class="cell reviewers"
- hidden$="[[_computeIsColumnHidden('Reviewers', visibleChangeTableColumns)]]"
- >
- <div>
- <template
- is="dom-repeat"
- items="[[_computePrimaryReviewers(change)]]"
- as="reviewer"
- indexAs="index"
- >
- <gr-account-link
- hideAvatar=""
- hideStatus=""
- firstName
- highlightAttention
- change="[[change]]"
- account="[[reviewer]]"
- ></gr-account-link
- ><span
- hidden$="[[_computeCommaHidden(index, change)]]"
- aria-hidden="true"
- >,
- </span>
- </template>
- <template is="dom-if" if="[[_computeAdditionalReviewersCount(change)]]">
- <span title="[[_computeAdditionalReviewersTitle(change, config)]]">
- +[[_computeAdditionalReviewersCount(change)]]
- </span>
- </template>
- </div>
- </td>
- <td
- class="cell comments"
- hidden$="[[_computeIsColumnHidden('Comments', visibleChangeTableColumns)]]"
- >
- <iron-icon
- hidden$="[[!change.unresolved_comment_count]]"
- icon="gr-icons:comment"
- ></iron-icon>
- <span>[[_computeComments(change.unresolved_comment_count)]]</span>
- </td>
- <td
- class="cell repo"
- hidden$="[[_computeIsColumnHidden('Repo', visibleChangeTableColumns)]]"
- >
- <a class="fullRepo" href$="[[_computeRepoUrl(change)]]">
- [[_computeRepoDisplay(change)]]
- </a>
- <a
- class="truncatedRepo"
- href$="[[_computeRepoUrl(change)]]"
- title$="[[_computeRepoDisplay(change)]]"
- >
- [[_computeTruncatedRepoDisplay(change)]]
- </a>
- </td>
- <td
- class="cell branch"
- hidden$="[[_computeIsColumnHidden('Branch', visibleChangeTableColumns)]]"
- >
- <a href$="[[_computeRepoBranchURL(change)]]"> [[change.branch]] </a>
- <template is="dom-if" if="[[change.topic]]">
- (<a href$="[[_computeTopicURL(change)]]"
- ><!--
- --><gr-limited-text limit="50" text="[[change.topic]]"> </gr-limited-text
- ><!--
- --></a
- >)
- </template>
- </td>
- <td
- class="cell updated"
- hidden$="[[_computeIsColumnHidden('Updated', visibleChangeTableColumns)]]"
- >
- <gr-date-formatter
- withTooltip
- date-str="[[_formatDate(change.updated)]]"
- ></gr-date-formatter>
- </td>
- <td
- class="cell submitted"
- hidden$="[[_computeIsColumnHidden('Submitted', visibleChangeTableColumns)]]"
- >
- <gr-date-formatter
- withTooltip
- date-str="[[_formatDate(change.submitted)]]"
- ></gr-date-formatter>
- </td>
- <td
- class="cell waiting"
- hidden$="[[_computeIsColumnHidden('Waiting', visibleChangeTableColumns)]]"
- >
- <gr-date-formatter
- withTooltip
- forceRelative
- relativeOptionNoAgo
- date-str="[[_computeWaiting(account, change)]]"
- ></gr-date-formatter>
- </td>
- <td
- class="cell size"
- hidden$="[[_computeIsColumnHidden('Size', visibleChangeTableColumns)]]"
- >
- <gr-tooltip-content has-tooltip title="[[_computeSizeTooltip(change)]]">
- <template is="dom-if" if="[[_changeSize]]">
- <span>[[_changeSize]]</span>
- </template>
- <template is="dom-if" if="[[!_changeSize]]">
- <span class="placeholder">--</span>
- </template>
- </gr-tooltip-content>
- </td>
- <template is="dom-repeat" items="[[labelNames]]" as="labelName">
- <td
- title$="[[_computeLabelTitle(change, labelName)]]"
- class$="[[_computeLabelClass(change, labelName)]]"
- >
- <template is="dom-if" if="[[_computeHasLabelIcon(change, labelName)]]">
- <iron-icon icon="[[_computeLabelIcon(change, labelName)]]"></iron-icon>
- </template>
- <template is="dom-if" if="[[!_computeHasLabelIcon(change, labelName)]]">
- <span>[[_computeLabelValue(change, labelName)]]</span>
- </template>
- </td>
- </template>
- <template
- is="dom-repeat"
- items="[[_dynamicCellEndpoints]]"
- as="pluginEndpointName"
- >
- <td class="cell endpoint">
- <gr-endpoint-decorator name$="[[pluginEndpointName]]">
- <gr-endpoint-param name="change" value="[[change]]">
- </gr-endpoint-param>
- </gr-endpoint-decorator>
- </td>
- </template>
-`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
index 34cb6eb..8932d52 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
@@ -15,10 +15,16 @@
* limitations under the License.
*/
+import {fixture} from '@open-wc/testing-helpers';
+import {html} from 'lit';
+import {SubmitRequirementResultInfo} from '../../../api/rest-api';
+import {getAppContext} from '../../../services/app-context';
import '../../../test/common-test-setup-karma';
import {
createAccountWithId,
createChange,
+ createSubmitRequirementExpressionInfo,
+ createSubmitRequirementResultInfo,
} from '../../../test/test-data-generators';
import {query, queryAndAssert, stubRestApi} from '../../../test/test-utils';
import {
@@ -28,6 +34,7 @@
RepoName,
TopicName,
} from '../../../types/common';
+import {StandardLabels} from '../../../utils/label-util';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {columnNames} from '../gr-change-list/gr-change-list';
import './gr-change-list-item';
@@ -47,314 +54,213 @@
let element: GrChangeListItem;
- setup(() => {
+ setup(async () => {
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
element = basicFixture.instantiate();
+ await element.updateComplete;
});
- test('_computeLabelCategory', () => {
+ test('computeLabelCategory', () => {
+ element.change = {
+ ...change,
+ labels: {},
+ };
assert.equal(
- element._computeLabelCategory({...change, labels: {}}, 'Verified'),
+ element.computeLabelCategory('Verified'),
LabelCategory.NOT_APPLICABLE
);
+ element.change.labels = {Verified: {approved: account, value: 1}};
assert.equal(
- element._computeLabelCategory(
- {...change, labels: {Verified: {approved: account, value: 1}}},
- 'Verified'
- ),
+ element.computeLabelCategory('Verified'),
LabelCategory.APPROVED
);
+ element.change.labels = {Verified: {rejected: account, value: -1}};
assert.equal(
- element._computeLabelCategory(
- {...change, labels: {Verified: {rejected: account, value: -1}}},
- 'Verified'
- ),
+ element.computeLabelCategory('Verified'),
LabelCategory.REJECTED
);
+ element.change.labels = {'Code-Review': {approved: account, value: 1}};
+ element.change.unresolved_comment_count = 1;
assert.equal(
- element._computeLabelCategory(
- {
- ...change,
- labels: {'Code-Review': {approved: account, value: 1}},
- unresolved_comment_count: 1,
- },
- 'Code-Review'
- ),
+ element.computeLabelCategory('Code-Review'),
LabelCategory.UNRESOLVED_COMMENTS
);
+ element.change.labels = {'Code-Review': {value: 1}};
+ element.change.unresolved_comment_count = 0;
assert.equal(
- element._computeLabelCategory(
- {...change, labels: {'Code-Review': {value: 1}}},
- 'Code-Review'
- ),
+ element.computeLabelCategory('Code-Review'),
LabelCategory.POSITIVE
);
+ element.change.labels = {'Code-Review': {value: -1}};
assert.equal(
- element._computeLabelCategory(
- {...change, labels: {'Code-Review': {value: -1}}},
- 'Code-Review'
- ),
+ element.computeLabelCategory('Code-Review'),
LabelCategory.NEGATIVE
);
+ element.change.labels = {'Code-Review': {value: -1}};
assert.equal(
- element._computeLabelCategory(
- {...change, labels: {'Code-Review': {value: -1}}},
- 'Verified'
- ),
+ element.computeLabelCategory('Verified'),
LabelCategory.NOT_APPLICABLE
);
});
- test('_computeLabelClass', () => {
+ test('computeLabelClass', () => {
+ element.change = {
+ ...change,
+ labels: {},
+ };
assert.equal(
- element._computeLabelClass({...change, labels: {}}, 'Verified'),
+ element.computeLabelClass('Verified'),
'cell label u-gray-background'
);
+ element.change.labels = {Verified: {approved: account, value: 1}};
+ assert.equal(element.computeLabelClass('Verified'), 'cell label u-green');
+ element.change.labels = {Verified: {rejected: account, value: -1}};
+ assert.equal(element.computeLabelClass('Verified'), 'cell label u-red');
+ element.change.labels = {'Code-Review': {value: 1}};
assert.equal(
- element._computeLabelClass(
- {...change, labels: {Verified: {approved: account, value: 1}}},
- 'Verified'
- ),
- 'cell label u-green'
- );
- assert.equal(
- element._computeLabelClass(
- {...change, labels: {Verified: {rejected: account, value: -1}}},
- 'Verified'
- ),
- 'cell label u-red'
- );
- assert.equal(
- element._computeLabelClass(
- {...change, labels: {'Code-Review': {value: 1}}},
- 'Code-Review'
- ),
+ element.computeLabelClass('Code-Review'),
'cell label u-green u-monospace'
);
+ element.change.labels = {'Code-Review': {value: -1}};
assert.equal(
- element._computeLabelClass(
- {...change, labels: {'Code-Review': {value: -1}}},
- 'Code-Review'
- ),
+ element.computeLabelClass('Code-Review'),
'cell label u-monospace u-red'
);
+ element.change.labels = {'Code-Review': {value: -1}};
assert.equal(
- element._computeLabelClass(
- {...change, labels: {'Code-Review': {value: -1}}},
- 'Verified'
- ),
+ element.computeLabelClass('Verified'),
'cell label u-gray-background'
);
});
- test('_computeLabelTitle', () => {
+ test('computeLabelTitle', () => {
+ element.change = {
+ ...change,
+ labels: {},
+ };
+ assert.equal(element.computeLabelTitle('Verified'), 'Label not applicable');
+
+ element.change.labels = {Verified: {approved: {name: 'Diffy'}}};
+ assert.equal(element.computeLabelTitle('Verified'), 'Verified by Diffy');
+
+ element.change.labels = {Verified: {approved: {name: 'Diffy'}}};
assert.equal(
- element._computeLabelTitle({...change, labels: {}}, 'Verified'),
+ element.computeLabelTitle('Code-Review'),
'Label not applicable'
);
+
+ element.change.labels = {Verified: {rejected: {name: 'Diffy'}}};
+ assert.equal(element.computeLabelTitle('Verified'), 'Verified by Diffy');
+
+ element.change.labels = {
+ 'Code-Review': {disliked: {name: 'Diffy'}, value: -1},
+ };
assert.equal(
- element._computeLabelTitle(
- {...change, labels: {Verified: {approved: {name: 'Diffy'}}}},
- 'Verified'
- ),
- 'Verified by Diffy'
- );
- assert.equal(
- element._computeLabelTitle(
- {...change, labels: {Verified: {approved: {name: 'Diffy'}}}},
- 'Code-Review'
- ),
- 'Label not applicable'
- );
- assert.equal(
- element._computeLabelTitle(
- {...change, labels: {Verified: {rejected: {name: 'Diffy'}}}},
- 'Verified'
- ),
- 'Verified by Diffy'
- );
- assert.equal(
- element._computeLabelTitle(
- {
- ...change,
- labels: {'Code-Review': {disliked: {name: 'Diffy'}, value: -1}},
- },
- 'Code-Review'
- ),
+ element.computeLabelTitle('Code-Review'),
'Code-Review by Diffy'
);
+
+ element.change.labels = {
+ 'Code-Review': {recommended: {name: 'Diffy'}, value: 1},
+ };
assert.equal(
- element._computeLabelTitle(
- {
- ...change,
- labels: {'Code-Review': {recommended: {name: 'Diffy'}, value: 1}},
- },
- 'Code-Review'
- ),
+ element.computeLabelTitle('Code-Review'),
'Code-Review by Diffy'
);
+
+ element.change.labels = {
+ 'Code-Review': {recommended: {name: 'Diffy'}, rejected: {name: 'Admin'}},
+ };
assert.equal(
- element._computeLabelTitle(
- {
- ...change,
- labels: {
- 'Code-Review': {
- recommended: {name: 'Diffy'},
- rejected: {name: 'Admin'},
- },
- },
- },
- 'Code-Review'
- ),
+ element.computeLabelTitle('Code-Review'),
'Code-Review by Admin'
);
+
+ element.change.labels = {
+ 'Code-Review': {approved: {name: 'Diffy'}, rejected: {name: 'Admin'}},
+ };
assert.equal(
- element._computeLabelTitle(
- {
- ...change,
- labels: {
- 'Code-Review': {
- approved: {name: 'Diffy'},
- rejected: {name: 'Admin'},
- },
- },
- },
- 'Code-Review'
- ),
+ element.computeLabelTitle('Code-Review'),
'Code-Review by Admin'
);
+
+ element.change.labels = {
+ 'Code-Review': {
+ recommended: {name: 'Diffy'},
+ disliked: {name: 'Admin'},
+ value: -1,
+ },
+ };
assert.equal(
- element._computeLabelTitle(
- {
- ...change,
- labels: {
- 'Code-Review': {
- recommended: {name: 'Diffy'},
- disliked: {name: 'Admin'},
- value: -1,
- },
- },
- },
- 'Code-Review'
- ),
+ element.computeLabelTitle('Code-Review'),
'Code-Review by Admin'
);
+
+ element.change.labels = {
+ 'Code-Review': {
+ approved: {name: 'Diffy'},
+ disliked: {name: 'Admin'},
+ value: -1,
+ },
+ };
assert.equal(
- element._computeLabelTitle(
- {
- ...change,
- labels: {
- 'Code-Review': {
- approved: {name: 'Diffy'},
- disliked: {name: 'Admin'},
- value: -1,
- },
- },
- },
- 'Code-Review'
- ),
+ element.computeLabelTitle('Code-Review'),
'Code-Review by Diffy'
);
+
+ element.change.labels = {'Code-Review': {approved: account, value: 1}};
+ element.change.unresolved_comment_count = 1;
assert.equal(
- element._computeLabelTitle(
- {
- ...change,
- labels: {'Code-Review': {approved: account, value: 1}},
- unresolved_comment_count: 1,
- },
- 'Code-Review'
- ),
+ element.computeLabelTitle('Code-Review'),
'1 unresolved comment'
);
+
+ element.change.labels = {
+ 'Code-Review': {approved: {name: 'Diffy'}, value: 1},
+ };
+ element.change.unresolved_comment_count = 1;
assert.equal(
- element._computeLabelTitle(
- {
- ...change,
- labels: {'Code-Review': {approved: {name: 'Diffy'}, value: 1}},
- unresolved_comment_count: 1,
- },
- 'Code-Review'
- ),
+ element.computeLabelTitle('Code-Review'),
'1 unresolved comment,\nCode-Review by Diffy'
);
+
+ element.change.labels = {'Code-Review': {approved: account, value: 1}};
+ element.change.unresolved_comment_count = 2;
assert.equal(
- element._computeLabelTitle(
- {
- ...change,
- labels: {'Code-Review': {approved: account, value: 1}},
- unresolved_comment_count: 2,
- },
- 'Code-Review'
- ),
+ element.computeLabelTitle('Code-Review'),
'2 unresolved comments'
);
});
- test('_computeLabelIcon', () => {
- assert.equal(
- element._computeLabelIcon({...change, labels: {}}, 'missingLabel'),
- ''
- );
- assert.equal(
- element._computeLabelIcon(
- {...change, labels: {Verified: {approved: account, value: 1}}},
- 'Verified'
- ),
- 'gr-icons:check'
- );
- assert.equal(
- element._computeLabelIcon(
- {
- ...change,
- labels: {'Code-Review': {approved: account, value: 1}},
- unresolved_comment_count: 1,
- },
- 'Code-Review'
- ),
- 'gr-icons:comment'
- );
+ test('computeLabelIcon', () => {
+ element.change = {
+ ...change,
+ labels: {},
+ };
+ assert.equal(element.computeLabelIcon('missingLabel'), '');
+ element.change.labels = {Verified: {approved: account, value: 1}};
+ assert.equal(element.computeLabelIcon('Verified'), 'gr-icons:check');
+ element.change.labels = {'Code-Review': {approved: account, value: 1}};
+ element.change.unresolved_comment_count = 1;
+ assert.equal(element.computeLabelIcon('Code-Review'), 'gr-icons:comment');
});
- test('_computeLabelValue', () => {
- assert.equal(
- element._computeLabelValue({...change, labels: {}}, 'Verified'),
- ''
- );
- assert.equal(
- element._computeLabelValue(
- {...change, labels: {Verified: {approved: account, value: 1}}},
- 'Verified'
- ),
- '✓'
- );
- assert.equal(
- element._computeLabelValue(
- {...change, labels: {Verified: {value: 1}}},
- 'Verified'
- ),
- '+1'
- );
- assert.equal(
- element._computeLabelValue(
- {...change, labels: {Verified: {value: -1}}},
- 'Verified'
- ),
- '-1'
- );
- assert.equal(
- element._computeLabelValue(
- {...change, labels: {Verified: {approved: account}}},
- 'Verified'
- ),
- '✓'
- );
- assert.equal(
- element._computeLabelValue(
- {...change, labels: {Verified: {rejected: account}}},
- 'Verified'
- ),
- '✕'
- );
+ test('computeLabelValue', () => {
+ element.change = {
+ ...change,
+ labels: {},
+ };
+ assert.equal(element.computeLabelValue('Verified'), '');
+ element.change.labels = {Verified: {approved: account, value: 1}};
+ assert.equal(element.computeLabelValue('Verified'), '✓');
+ element.change.labels = {Verified: {value: 1}};
+ assert.equal(element.computeLabelValue('Verified'), '+1');
+ element.change.labels = {Verified: {value: -1}};
+ assert.equal(element.computeLabelValue('Verified'), '-1');
+ element.change.labels = {Verified: {approved: account}};
+ assert.equal(element.computeLabelValue('Verified'), '✓');
+ element.change.labels = {Verified: {rejected: account}};
+ assert.equal(element.computeLabelValue('Verified'), '✕');
});
test('no hidden columns', async () => {
@@ -362,19 +268,19 @@
'Subject',
'Status',
'Owner',
- 'Assignee',
'Reviewers',
'Comments',
'Repo',
'Branch',
'Updated',
'Size',
+ ' Status ',
];
- await flush();
+ await element.updateComplete;
for (const column of columnNames) {
- const elementClass = '.' + column.toLowerCase();
+ const elementClass = '.' + column.trim().toLowerCase();
assert.isFalse(
queryAndAssert(element, elementClass).hasAttribute('hidden')
);
@@ -386,26 +292,22 @@
'Subject',
'Status',
'Owner',
- 'Assignee',
'Reviewers',
'Comments',
'Branch',
'Updated',
'Size',
+ ' Status ',
];
- await flush();
+ await element.updateComplete;
for (const column of columnNames) {
- const elementClass = '.' + column.toLowerCase();
+ const elementClass = '.' + column.trim().toLowerCase();
if (column === 'Repo') {
- assert.isTrue(
- queryAndAssert(element, elementClass).hasAttribute('hidden')
- );
+ assert.isNotOk(query(element, elementClass));
} else {
- assert.isFalse(
- queryAndAssert(element, elementClass).hasAttribute('hidden')
- );
+ assert.isOk(query(element, elementClass));
}
}
});
@@ -436,9 +338,7 @@
}
attSetIds.forEach(id => (element.change!.attention_set![id] = {account}));
- const actual = element
- ._computeReviewers(element.change)
- .map(r => r._account_id);
+ const actual = element.computeReviewers().map(r => r._account_id);
assert.deepEqual(actual, expected as AccountId[]);
}
@@ -468,84 +368,80 @@
test('random column does not exist', async () => {
element.visibleChangeTableColumns = ['Bad'];
- await flush();
+ await element.updateComplete;
const elementClass = '.bad';
assert.isNotOk(query(element, elementClass));
});
- test('assignee only displayed if there is one', async () => {
- element.change = change;
- await flush();
- assert.isNotOk(query(element, '.assignee gr-account-link'));
- assert.equal(
- queryAndAssert(element, '.assignee').textContent!.trim(),
- '--'
- );
+ test('TShirt sizing tooltip', () => {
element.change = {
...change,
- assignee: {
- name: 'test',
- status: 'test',
- },
+ insertions: NaN,
+ deletions: NaN,
};
- await flush();
- queryAndAssert(element, '.assignee gr-account-link');
- });
-
- test('TShirt sizing tooltip', () => {
- assert.equal(
- element._computeSizeTooltip({
- ...change,
- insertions: NaN,
- deletions: NaN,
- }),
- 'Size unknown'
- );
- assert.equal(
- element._computeSizeTooltip({...change, insertions: 0, deletions: 0}),
- 'Size unknown'
- );
- assert.equal(
- element._computeSizeTooltip({...change, insertions: 1, deletions: 2}),
- 'added 1, removed 2 lines'
- );
+ assert.equal(element.computeSizeTooltip(), 'Size unknown');
+ element.change = {
+ ...change,
+ insertions: 0,
+ deletions: 0,
+ };
+ assert.equal(element.computeSizeTooltip(), 'Size unknown');
+ element.change = {
+ ...change,
+ insertions: 1,
+ deletions: 2,
+ };
+ assert.equal(element.computeSizeTooltip(), 'added 1, removed 2 lines');
});
test('TShirt sizing', () => {
- assert.equal(
- element._computeChangeSize({
- ...change,
- insertions: NaN,
- deletions: NaN,
- }),
- null
- );
- assert.equal(
- element._computeChangeSize({...change, insertions: 1, deletions: 1}),
- 'XS'
- );
- assert.equal(
- element._computeChangeSize({...change, insertions: 9, deletions: 1}),
- 'S'
- );
- assert.equal(
- element._computeChangeSize({...change, insertions: 10, deletions: 200}),
- 'M'
- );
- assert.equal(
- element._computeChangeSize({...change, insertions: 99, deletions: 900}),
- 'L'
- );
- assert.equal(
- element._computeChangeSize({...change, insertions: 99, deletions: 999}),
- 'XL'
- );
+ element.change = {
+ ...change,
+ insertions: NaN,
+ deletions: NaN,
+ };
+ assert.equal(element.computeChangeSize(), null);
+
+ element.change = {
+ ...change,
+ insertions: 1,
+ deletions: 1,
+ };
+ assert.equal(element.computeChangeSize(), 'XS');
+
+ element.change = {
+ ...change,
+ insertions: 9,
+ deletions: 1,
+ };
+ assert.equal(element.computeChangeSize(), 'S');
+
+ element.change = {
+ ...change,
+ insertions: 10,
+ deletions: 200,
+ };
+ assert.equal(element.computeChangeSize(), 'M');
+
+ element.change = {
+ ...change,
+ insertions: 99,
+ deletions: 900,
+ };
+ assert.equal(element.computeChangeSize(), 'L');
+
+ element.change = {
+ ...change,
+ insertions: 99,
+ deletions: 999,
+ };
+ assert.equal(element.computeChangeSize(), 'XL');
});
test('change params passed to gr-navigation', async () => {
const navStub = sinon.stub(GerritNav);
element.change = change;
- await flush();
+ await element.updateComplete;
assert.deepEqual(navStub.getUrlForChange.lastCall.args, [change]);
assert.deepEqual(navStub.getUrlForProjectChanges.lastCall.args, [
@@ -565,14 +461,42 @@
]);
});
- test('_computeRepoDisplay', () => {
- assert.equal(element._computeRepoDisplay(change), 'host/a/test/repo');
- assert.equal(
- element._computeTruncatedRepoDisplay(change),
- 'host/…/test/repo'
- );
+ test('computeRepoDisplay', () => {
+ element.change = {...change};
+ assert.equal(element.computeRepoDisplay(), 'host/a/test/repo');
+ assert.equal(element.computeTruncatedRepoDisplay(), 'host/…/test/repo');
delete change.internalHost;
- assert.equal(element._computeRepoDisplay(change), 'a/test/repo');
- assert.equal(element._computeTruncatedRepoDisplay(change), '…/test/repo');
+ element.change = {...change};
+ assert.equal(element.computeRepoDisplay(), 'a/test/repo');
+ assert.equal(element.computeTruncatedRepoDisplay(), '…/test/repo');
+ });
+
+ test('renders requirement with new submit requirements', async () => {
+ sinon.stub(getAppContext().flagsService, 'isEnabled').returns(true);
+ const submitRequirement: SubmitRequirementResultInfo = {
+ ...createSubmitRequirementResultInfo(),
+ name: StandardLabels.CODE_REVIEW,
+ submittability_expression_result: {
+ ...createSubmitRequirementExpressionInfo(),
+ expression: 'label:Verified=MAX -label:Verified=MIN',
+ },
+ };
+ const change: ChangeInfo = {
+ ...createChange(),
+ submit_requirements: [submitRequirement],
+ unresolved_comment_count: 1,
+ };
+ const element = await fixture<GrChangeListItem>(
+ html`<gr-change-list-item
+ .change=${change}
+ .labelNames=${[StandardLabels.CODE_REVIEW]}
+ ></gr-change-list-item>`
+ );
+
+ const requirement = queryAndAssert(element, '.requirement');
+ expect(requirement).dom.to.equal(`<iron-icon
+ class="check-circle-filled"
+ icon="gr-icons:check-circle-filled">
+ </iron-icon>`);
});
});
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 2360312..142abaa 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
@@ -19,12 +19,8 @@
import '../gr-change-list/gr-change-list';
import '../gr-repo-header/gr-repo-header';
import '../gr-user-header/gr-user-header';
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-change-list-view_html';
import {page} from '../../../utils/page-wrapper-utils';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {customElement, property} from '@polymer/decorators';
import {AppElementParams} from '../../gr-app-types';
import {
AccountDetailInfo,
@@ -36,10 +32,14 @@
} from '../../../types/common';
import {ChangeStarToggleStarDetail} from '../../shared/gr-change-star/gr-change-star';
import {ChangeListViewState} from '../../../types/types';
-import {fireTitleChange} from '../../../utils/event-util';
-import {appContext} from '../../../services/app-context';
+import {fire, fireTitleChange} from '../../../utils/event-util';
+import {getAppContext} from '../../../services/app-context';
import {GerritView} from '../../../services/router/router-model';
import {RELOAD_DASHBOARD_INTERVAL_MS} from '../../../constants/constants';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, html, css} from 'lit';
+import {customElement, property, state, query} from 'lit/decorators';
+import {ValueChangedEvent} from '../../../types/events';
const LOOKUP_QUERY_PATTERNS: RegExp[] = [
/^\s*i?[0-9a-f]{7,40}\s*$/i, // CHANGE_ID
@@ -54,71 +54,61 @@
const LIMIT_OPERATOR_PATTERN = /\blimit:(\d+)/i;
-export interface GrChangeListView {
- $: {
- prevArrow: HTMLAnchorElement;
- nextArrow: HTMLAnchorElement;
- };
-}
-
@customElement('gr-change-list-view')
-export class GrChangeListView extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrChangeListView extends LitElement {
/**
* Fired when the title of the page should change.
*
* @event title-change
*/
- @property({type: Object, observer: '_paramsChanged'})
- params?: AppElementParams;
+ @query('#prevArrow') protected prevArrow?: HTMLAnchorElement;
- @property({type: Boolean, computed: '_computeLoggedIn(account)'})
- _loggedIn?: boolean;
+ @query('#nextArrow') protected nextArrow?: HTMLAnchorElement;
+
+ @property({type: Object})
+ params?: AppElementParams;
@property({type: Object})
account: AccountDetailInfo | null = null;
- @property({type: Object, notify: true})
+ @property({type: Object})
viewState: ChangeListViewState = {};
@property({type: Object})
preferences?: PreferencesInput;
- @property({type: Number})
- _changesPerPage?: number;
+ // private but used in test
+ @state() changesPerPage?: number;
- @property({type: String})
- _query = '';
+ // private but used in test
+ @state() query = '';
- @property({type: Number})
- _offset?: number;
+ // private but used in test
+ @state() offset?: number;
- @property({type: Array, observer: '_changesChanged'})
- _changes?: ChangeInfo[];
+ // private but used in test
+ @state() changes?: ChangeInfo[];
- @property({type: Boolean})
- _loading = true;
+ // private but used in test
+ @state() loading = true;
- @property({type: String})
- _userId: AccountId | EmailAddress | null = null;
+ // private but used in test
+ @state() userId: AccountId | EmailAddress | null = null;
- @property({type: String})
- _repo: RepoName | null = null;
+ // private but used in test
+ @state() repo: RepoName | null = null;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
- private reporting = appContext.reportingService;
+ private reporting = getAppContext().reportingService;
private lastVisibleTimestampMs = 0;
constructor() {
super();
- this.addEventListener('next-page', () => this._handleNextPage());
- this.addEventListener('previous-page', () => this._handlePreviousPage());
+ this.addEventListener('next-page', () => this.handleNextPage());
+ this.addEventListener('previous-page', () => this.handlePreviousPage());
this.addEventListener('reload', () => this.reload());
// We are not currently verifying if the view is actually visible. We rely
// on gr-app-element to restamp the component if view changes
@@ -137,37 +127,176 @@
override connectedCallback() {
super.connectedCallback();
- this._loadPreferences();
+ this.loadPreferences();
+ }
+
+ static override get styles() {
+ return [
+ sharedStyles,
+ css`
+ :host {
+ display: block;
+ }
+ .loading {
+ color: var(--deemphasized-text-color);
+ padding: var(--spacing-l);
+ }
+ gr-change-list {
+ width: 100%;
+ }
+ gr-user-header,
+ gr-repo-header {
+ border-bottom: 1px solid var(--border-color);
+ }
+ nav {
+ align-items: center;
+ display: flex;
+ height: 3rem;
+ justify-content: flex-end;
+ margin-right: 20px;
+ }
+ nav,
+ iron-icon {
+ color: var(--deemphasized-text-color);
+ }
+ iron-icon {
+ height: 1.85rem;
+ margin-left: 16px;
+ width: 1.85rem;
+ }
+ .hide {
+ display: none;
+ }
+ @media only screen and (max-width: 50em) {
+ .loading,
+ .error {
+ padding: 0 var(--spacing-l);
+ }
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ if (this.loading) return html`<div class="loading">Loading...</div>`;
+ const loggedIn = !!(this.account && Object.keys(this.account).length > 0);
+ return html`
+ <div>
+ ${this.renderRepoHeader()} ${this.renderUserHeader(loggedIn)}
+ <gr-change-list
+ .account=${this.account}
+ .changes=${this.changes}
+ .preferences=${this.preferences}
+ .selectedIndex=${this.viewState.selectedChangeIndex}
+ .showStar=${loggedIn}
+ @selected-index-changed=${(e: ValueChangedEvent<number>) => {
+ this.handleSelectedIndexChanged(e);
+ }}
+ @toggle-star=${(e: CustomEvent<ChangeStarToggleStarDetail>) => {
+ this.handleToggleStar(e);
+ }}
+ ></gr-change-list>
+ ${this.renderChangeListViewNav()}
+ </div>
+ `;
+ }
+
+ private renderRepoHeader() {
+ if (!this.repo) return;
+
+ return html` <gr-repo-header .repo=${this.repo}></gr-repo-header> `;
+ }
+
+ private renderUserHeader(loggedIn: boolean) {
+ if (!this.userId) return;
+
+ return html`
+ <gr-user-header
+ .userId=${this.userId}
+ showDashboardLink
+ .loggedIn=${loggedIn}
+ ></gr-user-header>
+ `;
+ }
+
+ private renderChangeListViewNav() {
+ if (this.loading || !this.changes || !this.changes.length) return;
+
+ return html`
+ <nav>
+ Page ${this.computePage()} ${this.renderPrevArrow()}
+ ${this.renderNextArrow()}
+ </nav>
+ `;
+ }
+
+ private renderPrevArrow() {
+ if (this.offset === 0) return;
+
+ return html`
+ <a id="prevArrow" href="${this.computeNavLink(-1)}">
+ <iron-icon icon="gr-icons:chevron-left" aria-label="Older"> </iron-icon>
+ </a>
+ `;
+ }
+
+ private renderNextArrow() {
+ if (
+ !(
+ this.changes?.length &&
+ this.changes[this.changes.length - 1]._more_changes
+ )
+ )
+ return;
+
+ return html`
+ <a id="nextArrow" href="${this.computeNavLink(1)}">
+ <iron-icon icon="gr-icons:chevron-right" aria-label="Newer">
+ </iron-icon>
+ </a>
+ `;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('params')) {
+ this.paramsChanged();
+ }
+
+ if (changedProperties.has('changes')) {
+ this.changesChanged();
+ }
}
reload() {
- if (this._loading) return;
- this._loading = true;
- this._getChanges().then(changes => {
- this._changes = changes || [];
- this._loading = false;
+ if (this.loading) return;
+ this.loading = true;
+ this.getChanges().then(changes => {
+ this.changes = changes || [];
+ this.loading = false;
});
}
- _paramsChanged(value: AppElementParams) {
- if (value.view !== GerritView.SEARCH) return;
+ private paramsChanged() {
+ const value = this.params;
+ if (!value || value.view !== GerritView.SEARCH) return;
- this._loading = true;
- this._query = value.query;
+ this.loading = true;
+ this.query = value.query;
const offset = Number(value.offset);
- this._offset = isNaN(offset) ? 0 : offset;
+ this.offset = isNaN(offset) ? 0 : offset;
if (
- this.viewState.query !== this._query ||
- this.viewState.offset !== this._offset
+ this.viewState.query !== this.query ||
+ this.viewState.offset !== this.offset
) {
- this.set('viewState.selectedChangeIndex', 0);
- this.set('viewState.query', this._query);
- this.set('viewState.offset', this._offset);
+ this.viewState.selectedChangeIndex = 0;
+ this.viewState.query = this.query;
+ this.viewState.offset = this.offset;
+ fire(this, 'view-state-changed', {value: this.viewState});
}
// 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));
+ setTimeout(() => fireTitleChange(this, this.query));
this.restApiService
.getPreferences()
@@ -175,33 +304,29 @@
if (!prefs) {
throw new Error('getPreferences returned undefined');
}
- this._changesPerPage = prefs.changes_per_page;
- return this._getChanges();
+ this.changesPerPage = prefs.changes_per_page;
+ return this.getChanges();
})
.then(changes => {
changes = changes || [];
- if (this._query && changes.length === 1) {
+ if (this.query && changes.length === 1) {
for (const queryPattern of LOOKUP_QUERY_PATTERNS) {
- if (this._query.match(queryPattern)) {
+ if (this.query.match(queryPattern)) {
// "Back"/"Forward" buttons work correctly only with
// opt_redirect options
- GerritNav.navigateToChange(
- changes[0],
- undefined,
- undefined,
- undefined,
- true
- );
+ GerritNav.navigateToChange(changes[0], {
+ redirect: true,
+ });
return;
}
}
}
- this._changes = changes;
- this._loading = false;
+ this.changes = changes;
+ this.loading = false;
});
}
- _loadPreferences() {
+ private loadPreferences() {
return this.restApiService.getLoggedIn().then(loggedIn => {
if (loggedIn) {
this.restApiService.getPreferences().then(preferences => {
@@ -213,15 +338,18 @@
});
}
- _getChanges() {
+ // private but used in test
+ getChanges() {
return this.restApiService.getChanges(
- this._changesPerPage,
- this._query,
- this._offset
+ this.changesPerPage,
+ this.query,
+ this.offset
);
}
- _limitFor(query: string, defaultLimit: number) {
+ // private but used in test
+ limitFor(query: string, defaultLimit?: number) {
+ if (defaultLimit === undefined) return 0;
const match = query.match(LIMIT_OPERATOR_PATTERN);
if (!match) {
return defaultLimit;
@@ -229,78 +357,53 @@
return Number(match[1]);
}
- _computeNavLink(
- query: string,
- offset: number | undefined,
- direction: number,
- changesPerPage: number
- ) {
- offset = offset ?? 0;
- const limit = this._limitFor(query, changesPerPage);
+ // private but used in test
+ computeNavLink(direction: number) {
+ const offset = this.offset ?? 0;
+ const limit = this.limitFor(this.query, this.changesPerPage);
const newOffset = Math.max(0, offset + limit * direction);
- return GerritNav.getUrlForSearchQuery(query, newOffset);
+ return GerritNav.getUrlForSearchQuery(this.query, newOffset);
}
- _computePrevArrowClass(offset?: number) {
- return offset === 0 ? 'hide' : '';
+ // private but used in test
+ handleNextPage() {
+ if (!this.nextArrow || !this.changesPerPage) return;
+ page.show(this.computeNavLink(1));
}
- _computeNextArrowClass(changes?: ChangeInfo[]) {
- const more = changes?.length && changes[changes.length - 1]._more_changes;
- return more ? '' : 'hide';
+ // private but used in test
+ handlePreviousPage() {
+ if (!this.prevArrow || !this.changesPerPage) return;
+ page.show(this.computeNavLink(-1));
}
- _computeNavClass(loading?: boolean) {
- return loading || !this._changes || !this._changes.length ? 'hide' : '';
- }
-
- _handleNextPage() {
- if (this.$.nextArrow.hidden || !this._changesPerPage) return;
- page.show(
- this._computeNavLink(this._query, this._offset, 1, this._changesPerPage)
- );
- }
-
- _handlePreviousPage() {
- if (this.$.prevArrow.hidden || !this._changesPerPage) return;
- page.show(
- this._computeNavLink(this._query, this._offset, -1, this._changesPerPage)
- );
- }
-
- _changesChanged(changes?: ChangeInfo[]) {
- this._userId = null;
- this._repo = null;
+ private changesChanged() {
+ this.userId = null;
+ this.repo = null;
+ const changes = this.changes;
if (!changes || !changes.length) {
return;
}
- if (USER_QUERY_PATTERN.test(this._query)) {
+ 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;
+ this.userId = userId;
return;
}
}
- if (REPO_QUERY_PATTERN.test(this._query)) {
- this._repo = changes[0].project;
+ if (REPO_QUERY_PATTERN.test(this.query)) {
+ this.repo = changes[0].project;
}
}
- _computeHeaderClass(id?: string) {
- return id ? '' : 'hide';
+ // private but used in test
+ computePage() {
+ if (this.offset === undefined || this.changesPerPage === undefined) return;
+ return this.offset / this.changesPerPage + 1;
}
- _computePage(offset?: number, changesPerPage?: number) {
- if (offset === undefined || changesPerPage === undefined) return;
- return offset / changesPerPage + 1;
- }
-
- _computeLoggedIn(account?: AccountDetailInfo) {
- return !!(account && Object.keys(account).length > 0);
- }
-
- _handleToggleStar(e: CustomEvent<ChangeStarToggleStarDetail>) {
+ private handleToggleStar(e: CustomEvent<ChangeStarToggleStarDetail>) {
if (e.detail.starred) {
this.reporting.reportInteraction('change-starred-from-change-list');
}
@@ -310,16 +413,17 @@
);
}
- /**
- * Returns `this` as the visibility observer target for the keyboard shortcut
- * mixin to decide whether shortcuts should be enabled or not.
- */
- _computeObserverTarget() {
- return this;
+ private handleSelectedIndexChanged(e: ValueChangedEvent<number>) {
+ if (!this.viewState) return;
+ this.viewState.selectedChangeIndex = e.detail.value;
+ fire(this, 'view-state-changed', {value: this.viewState});
}
}
declare global {
+ interface HTMLElementEventMap {
+ 'view-state-changed': ValueChangedEvent<ChangeListViewState>;
+ }
interface HTMLElementTagNameMap {
'gr-change-list-view': GrChangeListView;
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.ts
deleted file mode 100644
index 355ef45..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- display: block;
- }
- .loading {
- color: var(--deemphasized-text-color);
- padding: var(--spacing-l);
- }
- gr-change-list {
- width: 100%;
- }
- gr-user-header,
- gr-repo-header {
- border-bottom: 1px solid var(--border-color);
- }
- nav {
- align-items: center;
- display: flex;
- height: 3rem;
- justify-content: flex-end;
- margin-right: 20px;
- }
- nav,
- iron-icon {
- color: var(--deemphasized-text-color);
- }
- iron-icon {
- height: 1.85rem;
- margin-left: 16px;
- width: 1.85rem;
- }
- .hide {
- display: none;
- }
- @media only screen and (max-width: 50em) {
- .loading,
- .error {
- padding: 0 var(--spacing-l);
- }
- }
- </style>
- <div class="loading" hidden$="[[!_loading]]" hidden="">Loading...</div>
- <div hidden$="[[_loading]]" hidden="">
- <gr-repo-header
- repo="[[_repo]]"
- class$="[[_computeHeaderClass(_repo)]]"
- ></gr-repo-header>
- <gr-user-header
- user-id="[[_userId]]"
- showDashboardLink=""
- logged-in="[[_loggedIn]]"
- class$="[[_computeHeaderClass(_userId)]]"
- ></gr-user-header>
- <gr-change-list
- account="[[account]]"
- changes="{{_changes}}"
- preferences="[[preferences]]"
- selected-index="{{viewState.selectedChangeIndex}}"
- show-star="[[_loggedIn]]"
- on-toggle-star="_handleToggleStar"
- observer-target="[[_computeObserverTarget()]]"
- ></gr-change-list>
- <nav class$="[[_computeNavClass(_loading)]]">
- Page [[_computePage(_offset, _changesPerPage)]]
- <a
- id="prevArrow"
- href$="[[_computeNavLink(_query, _offset, -1, _changesPerPage)]]"
- class$="[[_computePrevArrowClass(_offset)]]"
- >
- <iron-icon icon="gr-icons:chevron-left" aria-label="Older"> </iron-icon>
- </a>
- <a
- id="nextArrow"
- href$="[[_computeNavLink(_query, _offset, 1, _changesPerPage)]]"
- class$="[[_computeNextArrowClass(_changes)]]"
- >
- <iron-icon icon="gr-icons:chevron-right" aria-label="Newer">
- </iron-icon>
- </a>
- </nav>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js
deleted file mode 100644
index 86b2fd1..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js
+++ /dev/null
@@ -1,249 +0,0 @@
-/**
- * @license
- * 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.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-change-list-view.js';
-import {page} from '../../../utils/page-wrapper-utils.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import 'lodash/lodash.js';
-import {mockPromise, stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-change-list-view');
-
-const CHANGE_ID = 'IcA3dAB3edAB9f60B8dcdA6ef71A75980e4B7127';
-const COMMIT_HASH = '12345678';
-
-suite('gr-change-list-view tests', () => {
- let element;
-
- setup(() => {
- stubRestApi('getLoggedIn').returns(Promise.resolve(false));
- stubRestApi('getChanges').returns(Promise.resolve([]));
- stubRestApi('getAccountDetails').returns(Promise.resolve({}));
- stubRestApi('getAccountStatus').returns(Promise.resolve({}));
- element = basicFixture.instantiate();
- });
-
- teardown(async () => {
- await flush();
- });
-
- test('_computePage', () => {
- assert.equal(element._computePage(0, 25), 1);
- assert.equal(element._computePage(50, 25), 3);
- });
-
- test('_limitFor', () => {
- const defaultLimit = 25;
- const _limitFor = q => element._limitFor(q, defaultLimit);
- assert.equal(_limitFor(''), defaultLimit);
- assert.equal(_limitFor('limit:10'), 10);
- assert.equal(_limitFor('xlimit:10'), defaultLimit);
- assert.equal(_limitFor('x(limit:10'), 10);
- });
-
- test('_computeNavLink', () => {
- const getUrlStub = sinon.stub(GerritNav, 'getUrlForSearchQuery')
- .returns('');
- const query = 'status:open';
- let offset = 0;
- let direction = 1;
- const changesPerPage = 5;
-
- element._computeNavLink(query, offset, direction, changesPerPage);
- assert.equal(getUrlStub.lastCall.args[1], 5);
-
- direction = -1;
- element._computeNavLink(query, offset, direction, changesPerPage);
- assert.equal(getUrlStub.lastCall.args[1], 0);
-
- offset = 5;
- direction = 1;
- element._computeNavLink(query, offset, direction, changesPerPage);
- assert.equal(getUrlStub.lastCall.args[1], 10);
- });
-
- test('_computePrevArrowClass', () => {
- let offset = 0;
- assert.equal(element._computePrevArrowClass(offset), 'hide');
- offset = 5;
- assert.equal(element._computePrevArrowClass(offset), '');
- });
-
- test('_computeNextArrowClass', () => {
- let changes = _.times(25, _.constant({_more_changes: true}));
- assert.equal(element._computeNextArrowClass(changes), '');
- changes = _.times(25, _.constant({}));
- assert.equal(element._computeNextArrowClass(changes), 'hide');
- });
-
- test('_computeNavClass', () => {
- let loading = true;
- assert.equal(element._computeNavClass(loading), 'hide');
- loading = false;
- assert.equal(element._computeNavClass(loading), 'hide');
- element._changes = [];
- assert.equal(element._computeNavClass(loading), 'hide');
- element._changes = _.times(5, _.constant({}));
- assert.equal(element._computeNavClass(loading), '');
- });
-
- test('_handleNextPage', () => {
- const showStub = sinon.stub(page, 'show');
- element._changesPerPage = 10;
- element.$.nextArrow.hidden = true;
- element._handleNextPage();
- assert.isFalse(showStub.called);
- element.$.nextArrow.hidden = false;
- element._handleNextPage();
- assert.isTrue(showStub.called);
- });
-
- test('_handlePreviousPage', () => {
- const showStub = sinon.stub(page, 'show');
- element._changesPerPage = 10;
- element.$.prevArrow.hidden = true;
- element._handlePreviousPage();
- assert.isFalse(showStub.called);
- element.$.prevArrow.hidden = false;
- element._handlePreviousPage();
- assert.isTrue(showStub.called);
- });
-
- test('_userId query', async () => {
- assert.isNull(element._userId);
- element._query = 'owner: foo@bar';
- element._changes = [{owner: {email: 'foo@bar'}}];
- await flush();
- assert.equal(element._userId, 'foo@bar');
-
- element._query = 'foo bar baz';
- element._changes = [{owner: {email: 'foo@bar'}}];
- assert.isNull(element._userId);
- });
-
- test('_userId query without email', async () => {
- assert.isNull(element._userId);
- element._query = 'owner: foo@bar';
- element._changes = [{owner: {}}];
- await flush();
- assert.isNull(element._userId);
- });
-
- test('_repo query', async () => {
- assert.isNull(element._repo);
- element._query = 'project: test-repo';
- element._changes = [{owner: {email: 'foo@bar'}, project: 'test-repo'}];
- await flush();
- assert.equal(element._repo, 'test-repo');
- element._query = 'foo bar baz';
- element._changes = [{owner: {email: 'foo@bar'}}];
- assert.isNull(element._repo);
- });
-
- test('_repo query with open status', async () => {
- assert.isNull(element._repo);
- element._query = 'project:test-repo status:open';
- element._changes = [{owner: {email: 'foo@bar'}, project: 'test-repo'}];
- await flush();
- assert.equal(element._repo, 'test-repo');
- element._query = 'foo bar baz';
- element._changes = [{owner: {email: 'foo@bar'}}];
- assert.isNull(element._repo);
- });
-
- suite('query based navigation', () => {
- setup(() => {
- });
-
- teardown(async () => {
- await flush();
- sinon.restore();
- });
-
- test('Searching for a change ID redirects to change', async () => {
- const change = {_number: 1};
- sinon.stub(element, '_getChanges')
- .returns(Promise.resolve([change]));
- const promise = mockPromise();
- sinon.stub(GerritNav, 'navigateToChange').callsFake(
- (url, opt_patchNum, opt_basePatchNum, opt_isEdit, opt_redirect) => {
- assert.equal(url, change);
- assert.isTrue(opt_redirect);
- promise.resolve();
- });
-
- element.params = {view: GerritNav.View.SEARCH, query: CHANGE_ID};
- await promise;
- });
-
- test('Searching for a change num redirects to change', async () => {
- const change = {_number: 1};
- sinon.stub(element, '_getChanges')
- .returns(Promise.resolve([change]));
- const promise = mockPromise();
- sinon.stub(GerritNav, 'navigateToChange').callsFake(
- (url, opt_patchNum, opt_basePatchNum, opt_isEdit, opt_redirect) => {
- assert.equal(url, change);
- assert.isTrue(opt_redirect);
- promise.resolve();
- });
-
- element.params = {view: GerritNav.View.SEARCH, query: '1'};
- await promise;
- });
-
- test('Commit hash redirects to change', async () => {
- const change = {_number: 1};
- sinon.stub(element, '_getChanges')
- .returns(Promise.resolve([change]));
- const promise = mockPromise();
- sinon.stub(GerritNav, 'navigateToChange').callsFake(
- (url, opt_patchNum, opt_basePatchNum, opt_isEdit, opt_redirect) => {
- assert.equal(url, change);
- assert.isTrue(opt_redirect);
- promise.resolve();
- });
-
- element.params = {view: GerritNav.View.SEARCH, query: COMMIT_HASH};
- await promise;
- });
-
- test('Searching for an invalid change ID searches', async () => {
- sinon.stub(element, '_getChanges')
- .returns(Promise.resolve([]));
- const stub = sinon.stub(GerritNav, 'navigateToChange');
-
- element.params = {view: GerritNav.View.SEARCH, query: CHANGE_ID};
- await flush();
-
- assert.isFalse(stub.called);
- });
-
- test('Change ID with multiple search results searches', async () => {
- sinon.stub(element, '_getChanges')
- .returns(Promise.resolve([{}, {}]));
- const stub = sinon.stub(GerritNav, 'navigateToChange');
-
- element.params = {view: GerritNav.View.SEARCH, query: CHANGE_ID};
- await flush();
-
- assert.isFalse(stub.called);
- });
- });
-});
-
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
new file mode 100644
index 0000000..0639620
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.ts
@@ -0,0 +1,308 @@
+/**
+ * @license
+ * 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.
+ */
+
+import '../../../test/common-test-setup-karma.js';
+import './gr-change-list-view';
+import {GrChangeListView} from './gr-change-list-view';
+import {page} from '../../../utils/page-wrapper-utils';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import 'lodash/lodash';
+import {mockPromise, query, stubRestApi} from '../../../test/test-utils';
+import {createChange} from '../../../test/test-data-generators.js';
+import {
+ ChangeInfo,
+ EmailAddress,
+ NumericChangeId,
+ RepoName,
+} from '../../../api/rest-api.js';
+
+const basicFixture = fixtureFromElement('gr-change-list-view');
+
+const CHANGE_ID = 'IcA3dAB3edAB9f60B8dcdA6ef71A75980e4B7127';
+const COMMIT_HASH = '12345678';
+
+suite('gr-change-list-view tests', () => {
+ let element: GrChangeListView;
+
+ setup(async () => {
+ stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+ stubRestApi('getChanges').returns(Promise.resolve([]));
+ stubRestApi('getAccountDetails').returns(Promise.resolve(undefined));
+ stubRestApi('getAccountStatus').returns(Promise.resolve(undefined));
+ element = basicFixture.instantiate();
+ await element.updateComplete;
+ });
+
+ teardown(async () => {
+ await element.updateComplete;
+ });
+
+ test('computePage', () => {
+ element.offset = 0;
+ element.changesPerPage = 25;
+ assert.equal(element.computePage(), 1);
+ element.offset = 50;
+ element.changesPerPage = 25;
+ assert.equal(element.computePage(), 3);
+ });
+
+ test('limitFor', () => {
+ const defaultLimit = 25;
+ const limitFor = (q: string) => element.limitFor(q, defaultLimit);
+ assert.equal(limitFor(''), defaultLimit);
+ assert.equal(limitFor('limit:10'), 10);
+ assert.equal(limitFor('xlimit:10'), defaultLimit);
+ assert.equal(limitFor('x(limit:10'), 10);
+ });
+
+ test('computeNavLink', () => {
+ const getUrlStub = sinon
+ .stub(GerritNav, 'getUrlForSearchQuery')
+ .returns('');
+ element.query = 'status:open';
+ element.offset = 0;
+ element.changesPerPage = 5;
+ let direction = 1;
+
+ element.computeNavLink(direction);
+ assert.equal(getUrlStub.lastCall.args[1], 5);
+
+ direction = -1;
+ element.computeNavLink(direction);
+ assert.equal(getUrlStub.lastCall.args[1], 0);
+
+ element.offset = 5;
+ direction = 1;
+ element.computeNavLink(direction);
+ assert.equal(getUrlStub.lastCall.args[1], 10);
+ });
+
+ test('prevArrow', async () => {
+ element.changes = _.times(25, _.constant(createChange())) as ChangeInfo[];
+ element.offset = 0;
+ element.loading = false;
+ await element.updateComplete;
+ assert.isNotOk(query(element, '#prevArrow'));
+
+ element.offset = 5;
+ await element.updateComplete;
+ assert.isOk(query(element, '#prevArrow'));
+ });
+
+ test('nextArrow', async () => {
+ element.changes = _.times(
+ 25,
+ _.constant({...createChange(), _more_changes: true})
+ ) as ChangeInfo[];
+ element.loading = false;
+ await element.updateComplete;
+ assert.isOk(query(element, '#nextArrow'));
+
+ element.changes = _.times(25, _.constant(createChange())) as ChangeInfo[];
+ await element.updateComplete;
+ assert.isNotOk(query(element, '#nextArrow'));
+ });
+
+ test('handleNextPage', async () => {
+ const showStub = sinon.stub(page, 'show');
+ element.changes = _.times(25, _.constant(createChange())) as ChangeInfo[];
+ element.changesPerPage = 10;
+ element.loading = false;
+ await element.updateComplete;
+ element.handleNextPage();
+ assert.isFalse(showStub.called);
+
+ element.changes = _.times(
+ 25,
+ _.constant({...createChange(), _more_changes: true})
+ ) as ChangeInfo[];
+ element.loading = false;
+ await element.updateComplete;
+ element.handleNextPage();
+ assert.isTrue(showStub.called);
+ });
+
+ test('handlePreviousPage', async () => {
+ const showStub = sinon.stub(page, 'show');
+ element.offset = 0;
+ element.changes = _.times(25, _.constant(createChange())) as ChangeInfo[];
+ element.changesPerPage = 10;
+ element.loading = false;
+ await element.updateComplete;
+ element.handlePreviousPage();
+ assert.isFalse(showStub.called);
+
+ element.offset = 25;
+ await element.updateComplete;
+ 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', () => {
+ setup(() => {});
+
+ teardown(async () => {
+ await element.updateComplete;
+ sinon.restore();
+ });
+
+ test('Searching for a change ID redirects to change', async () => {
+ const change = {...createChange(), _number: 1 as NumericChangeId};
+ sinon.stub(element, 'getChanges').returns(Promise.resolve([change]));
+ const promise = mockPromise();
+ sinon.stub(GerritNav, 'navigateToChange').callsFake((url, opt) => {
+ assert.equal(url, change);
+ assert.isTrue(opt!.redirect);
+ promise.resolve();
+ });
+
+ element.params = {
+ view: GerritNav.View.SEARCH,
+ query: CHANGE_ID,
+ offset: '',
+ };
+ await promise;
+ });
+
+ test('Searching for a change num redirects to change', async () => {
+ const change = {...createChange(), _number: 1 as NumericChangeId};
+ sinon.stub(element, 'getChanges').returns(Promise.resolve([change]));
+ const promise = mockPromise();
+ sinon.stub(GerritNav, 'navigateToChange').callsFake((url, opt) => {
+ assert.equal(url, change);
+ assert.isTrue(opt!.redirect);
+ promise.resolve();
+ });
+
+ element.params = {view: GerritNav.View.SEARCH, query: '1', offset: ''};
+ await promise;
+ });
+
+ test('Commit hash redirects to change', async () => {
+ const change = {...createChange(), _number: 1 as NumericChangeId};
+ sinon.stub(element, 'getChanges').returns(Promise.resolve([change]));
+ const promise = mockPromise();
+ sinon.stub(GerritNav, 'navigateToChange').callsFake((url, opt) => {
+ assert.equal(url, change);
+ assert.isTrue(opt!.redirect);
+ promise.resolve();
+ });
+
+ element.params = {
+ view: GerritNav.View.SEARCH,
+ query: COMMIT_HASH,
+ offset: '',
+ };
+ await promise;
+ });
+
+ test('Searching for an invalid change ID searches', async () => {
+ sinon.stub(element, 'getChanges').returns(Promise.resolve([]));
+ const stub = sinon.stub(GerritNav, 'navigateToChange');
+
+ element.params = {
+ view: GerritNav.View.SEARCH,
+ query: CHANGE_ID,
+ offset: '',
+ };
+ await element.updateComplete;
+
+ assert.isFalse(stub.called);
+ });
+
+ test('Change ID with multiple search results searches', async () => {
+ sinon.stub(element, 'getChanges').returns(Promise.resolve(undefined));
+ const stub = sinon.stub(GerritNav, 'navigateToChange');
+
+ element.params = {
+ view: GerritNav.View.SEARCH,
+ query: CHANGE_ID,
+ offset: '',
+ };
+ await element.updateComplete;
+
+ assert.isFalse(stub.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 a79dd8e..dfe4720b 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
@@ -15,30 +15,18 @@
* limitations under the License.
*/
-import '../../../styles/gr-change-list-styles';
import '../../shared/gr-cursor-manager/gr-cursor-manager';
import '../gr-change-list-item/gr-change-list-item';
-import '../../../styles/shared-styles';
+import {GrChangeListItem} from '../gr-change-list-item/gr-change-list-item';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
-import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-change-list_html';
-import {appContext} from '../../../services/app-context';
-import {
- KeyboardShortcutMixin,
- Shortcut,
- ShortcutListener,
-} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
+import {getAppContext} from '../../../services/app-context';
import {
GerritNav,
- DashboardSection,
YOUR_TURN,
CLOSED,
} 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 {isOwner} from '../../../utils/change-util';
-import {customElement, property, observe} from '@polymer/decorators';
import {GrCursorManager} from '../../shared/gr-cursor-manager/gr-cursor-manager';
import {
AccountInfo,
@@ -46,48 +34,52 @@
ServerInfo,
PreferencesInput,
} from '../../../types/common';
-import {hasAttention} from '../../../utils/attention-set-util';
-import {fireEvent, fireReload} from '../../../utils/event-util';
+import {fire, fireEvent, fireReload} from '../../../utils/event-util';
import {ScrollMode} from '../../../constants/constants';
-import {listen} from '../../../services/shortcuts/shortcuts-service';
+import {
+ getRequirements,
+ showNewSubmitRequirements,
+} from '../../../utils/label-util';
+import {addGlobalShortcut, Key} from '../../../utils/dom-util';
+import {unique} from '../../../utils/common-util';
+import {changeListStyles} from '../../../styles/gr-change-list-styles';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, html, css} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+import {ShortcutController} from '../../lit/shortcut-controller';
+import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
+import {queryAll} from '../../../utils/common-util';
+import {ValueChangedEvent} from '../../../types/events';
const NUMBER_FIXED_COLUMNS = 3;
-const CLOSED_STATUS = ['MERGED', 'ABANDONED'];
const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--';
const MAX_SHORTCUT_CHARS = 5;
export const columnNames = [
'Subject',
+ // TODO(milutin) - remove once Submit Requirements are rolled out.
'Status',
'Owner',
- 'Assignee',
'Reviewers',
'Comments',
'Repo',
'Branch',
'Updated',
'Size',
+ ' Status ', // spaces to differentiate from old 'Status'
];
export interface ChangeListSection {
+ countLabel?: string;
+ isOutgoing?: boolean;
name?: string;
query?: string;
results: ChangeInfo[];
}
-export interface GrChangeList {
- $: {};
-}
-
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
@customElement('gr-change-list')
-export class GrChangeList extends base {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrChangeList extends LitElement {
/**
* Fired when next page key shortcut was pressed.
*
@@ -107,7 +99,7 @@
@property({type: Object})
account: AccountInfo | undefined = undefined;
- @property({type: Array, observer: '_changesChanged'})
+ @property({type: Array})
changes?: ChangeInfo[];
/**
@@ -117,13 +109,9 @@
@property({type: Array})
sections: ChangeListSection[] = [];
- @property({type: Array, computed: '_computeLabelNames(sections)'})
- labelNames?: string[];
+ @state() private dynamicHeaderEndpoints?: string[];
- @property({type: Array})
- _dynamicHeaderEndpoints?: string[];
-
- @property({type: Number, notify: true})
+ @property({type: Number})
selectedIndex?: number;
@property({type: Boolean})
@@ -147,27 +135,14 @@
@property({type: Boolean})
isCursorMoving = false;
- @property({type: Object})
- _config?: ServerInfo;
+ // private but used in test
+ @state() config?: ServerInfo;
- private readonly flagsService = appContext.flagsService;
+ private readonly flagsService = getAppContext().flagsService;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
- override keyboardShortcuts(): ShortcutListener[] {
- return [
- listen(Shortcut.CURSOR_NEXT_CHANGE, _ => this._nextChange()),
- listen(Shortcut.CURSOR_PREV_CHANGE, _ => this._prevChange()),
- listen(Shortcut.NEXT_PAGE, _ => this._nextPage()),
- listen(Shortcut.PREV_PAGE, _ => this._prevPage()),
- listen(Shortcut.OPEN_CHANGE, _ => this.openChange()),
- listen(Shortcut.TOGGLE_CHANGE_REVIEWED, _ =>
- this._toggleChangeReviewed()
- ),
- listen(Shortcut.TOGGLE_CHANGE_STAR, _ => this._toggleChangeStar()),
- listen(Shortcut.REFRESH_CHANGE_LIST, _ => this._refreshChangeList()),
- ];
- }
+ private readonly shortcuts = new ShortcutController(this);
private cursor = new GrCursorManager();
@@ -175,22 +150,33 @@
super();
this.cursor.scrollMode = ScrollMode.KEEP_VISIBLE;
this.cursor.focusOnMove = true;
- this.addEventListener('keydown', e => this._scopedKeydownHandler(e));
- }
-
- override ready() {
- super.ready();
- this.restApiService.getConfig().then(config => {
- this._config = config;
- });
+ this.shortcuts.addAbstract(Shortcut.CURSOR_NEXT_CHANGE, () =>
+ this.nextChange()
+ );
+ this.shortcuts.addAbstract(Shortcut.CURSOR_PREV_CHANGE, () =>
+ this.prevChange()
+ );
+ this.shortcuts.addAbstract(Shortcut.NEXT_PAGE, () => this.nextPage());
+ this.shortcuts.addAbstract(Shortcut.PREV_PAGE, () => this.prevPage());
+ this.shortcuts.addAbstract(Shortcut.OPEN_CHANGE, () => this.openChange());
+ this.shortcuts.addAbstract(Shortcut.TOGGLE_CHANGE_STAR, () =>
+ this.toggleChangeStar()
+ );
+ this.shortcuts.addAbstract(Shortcut.REFRESH_CHANGE_LIST, () =>
+ this.refreshChangeList()
+ );
+ addGlobalShortcut({key: Key.ENTER}, () => this.openChange());
}
override connectedCallback() {
super.connectedCallback();
+ this.restApiService.getConfig().then(config => {
+ this.config = config;
+ });
getPluginLoader()
.awaitPluginsLoaded()
.then(() => {
- this._dynamicHeaderEndpoints =
+ this.dynamicHeaderEndpoints =
getPluginEndpoints().getDynamicEndpoints('change-list-header');
});
}
@@ -200,68 +186,279 @@
super.disconnectedCallback();
}
- /**
- * shortcut-service catches keyboard events globally. Some keyboard
- * events must be scoped to a component level (e.g. `enter`) in order to not
- * override native browser functionality.
- *
- * Context: Issue 7294
- */
- _scopedKeydownHandler(e: KeyboardEvent) {
- if (e.keyCode === 13) {
- // Enter.
- this.openChange();
- }
+ static override get styles() {
+ return [
+ changeListStyles,
+ fontStyles,
+ sharedStyles,
+ css`
+ #changeList {
+ border-collapse: collapse;
+ width: 100%;
+ }
+ .section-count-label {
+ color: var(--deemphasized-text-color);
+ font-family: var(--font-family);
+ font-size: var(--font-size-small);
+ font-weight: var(--font-weight-normal);
+ line-height: var(--line-height-small);
+ }
+ a.section-title:hover {
+ text-decoration: none;
+ }
+ a.section-title:hover .section-count-label {
+ text-decoration: none;
+ }
+ a.section-title:hover .section-name {
+ text-decoration: underline;
+ }
+ `,
+ ];
}
- _lowerCase(column: string) {
- return column.toLowerCase();
+ override render() {
+ const labelNames = this.computeLabelNames(this.sections);
+ return html`
+ <table id="changeList">
+ ${this.sections.map((changeSection, sectionIndex) =>
+ this.renderSections(changeSection, sectionIndex, labelNames)
+ )}
+ </table>
+ `;
}
- @observe('account', 'preferences', '_config')
- _computePreferences(
- account?: AccountInfo,
- preferences?: PreferencesInput,
- config?: ServerInfo
+ private renderSections(
+ changeSection: ChangeListSection,
+ sectionIndex: number,
+ labelNames: string[]
) {
- if (!config) {
- return;
+ return html`
+ ${this.renderSectionHeader(changeSection, labelNames)}
+ <tbody class="groupContent">
+ ${this.isEmpty(changeSection)
+ ? this.renderNoChangesRow(changeSection, labelNames)
+ : this.renderColumnHeaders(changeSection, labelNames)}
+ ${changeSection.results.map((change, index) =>
+ this.renderChangeRow(
+ changeSection,
+ change,
+ index,
+ sectionIndex,
+ labelNames
+ )
+ )}
+ </tbody>
+ `;
+ }
+
+ private renderSectionHeader(
+ changeSection: ChangeListSection,
+ labelNames: string[]
+ ) {
+ if (!changeSection.name) return;
+
+ return html`
+ <tbody>
+ <tr class="groupHeader">
+ <td aria-hidden="true" class="leftPadding"></td>
+ <td aria-hidden="true" class="star" ?hidden=${!this.showStar}></td>
+ <td
+ class="cell"
+ colspan="${this.computeColspan(changeSection, labelNames)}"
+ >
+ <h2 class="heading-3">
+ <a
+ href="${this.sectionHref(changeSection.query)}"
+ class="section-title"
+ >
+ <span class="section-name">${changeSection.name}</span>
+ <span class="section-count-label"
+ >${changeSection.countLabel}</span
+ >
+ </a>
+ </h2>
+ </td>
+ </tr>
+ </tbody>
+ `;
+ }
+
+ private renderNoChangesRow(
+ changeSection: ChangeListSection,
+ labelNames: string[]
+ ) {
+ return html`
+ <tr class="noChanges">
+ <td class="leftPadding" aria-hidden="true"></td>
+ <td
+ class="star"
+ ?aria-hidden=${!this.showStar}
+ ?hidden=${!this.showStar}
+ ></td>
+ <td
+ class="cell"
+ colspan="${this.computeColspan(changeSection, labelNames)}"
+ >
+ ${this.getSpecialEmptySlot(changeSection)
+ ? html`<slot
+ name="${this.getSpecialEmptySlot(changeSection)}"
+ ></slot>`
+ : 'No changes'}
+ </td>
+ </tr>
+ `;
+ }
+
+ private renderColumnHeaders(
+ changeSection: ChangeListSection,
+ labelNames: string[]
+ ) {
+ return html`
+ <tr class="groupTitle">
+ <td class="leftPadding" aria-hidden="true"></td>
+ <td
+ class="star"
+ aria-label="Star status column"
+ ?hidden=${!this.showStar}
+ ></td>
+ <td class="number" ?hidden=${!this.showNumber}>#</td>
+ ${this.computeColumns(changeSection).map(item =>
+ this.renderHeaderCell(item)
+ )}
+ ${labelNames?.map(labelName => this.renderLabelHeader(labelName))}
+ ${this.dynamicHeaderEndpoints?.map(pluginHeader =>
+ this.renderEndpointHeader(pluginHeader)
+ )}
+ </tr>
+ `;
+ }
+
+ private renderHeaderCell(item: string) {
+ return html`<td class="${item.toLowerCase()}">${item}</td>`;
+ }
+
+ private renderLabelHeader(labelName: string) {
+ return html`
+ <td class="label" title="${labelName}">
+ ${this.computeLabelShortcut(labelName)}
+ </td>
+ `;
+ }
+
+ private renderEndpointHeader(pluginHeader: string) {
+ return html`
+ <td class="endpoint">
+ <gr-endpoint-decorator .name="${pluginHeader}"></gr-endpoint-decorator>
+ </td>
+ `;
+ }
+
+ private renderChangeRow(
+ changeSection: ChangeListSection,
+ change: ChangeInfo,
+ index: number,
+ sectionIndex: number,
+ labelNames: string[]
+ ) {
+ const ariaLabel = this.computeAriaLabel(change, changeSection.name);
+ const selected = this.computeItemSelected(
+ sectionIndex,
+ index,
+ this.selectedIndex
+ );
+ const tabindex = this.computeTabIndex(
+ sectionIndex,
+ index,
+ this.isCursorMoving,
+ this.selectedIndex
+ );
+ const visibleChangeTableColumns = this.computeColumns(changeSection);
+ return html`
+ <gr-change-list-item
+ .account=${this.account}
+ ?selected=${selected}
+ .change=${change}
+ .config=${this.config}
+ .sectionName=${changeSection.name}
+ .visibleChangeTableColumns=${visibleChangeTableColumns}
+ .showNumber=${this.showNumber}
+ .showStar=${this.showStar}
+ ?tabindex=${tabindex}
+ .labelNames=${labelNames}
+ aria-label=${ariaLabel}
+ ></gr-change-list-item>
+ `;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (
+ changedProperties.has('account') ||
+ changedProperties.has('preferences') ||
+ changedProperties.has('config') ||
+ changedProperties.has('sections')
+ ) {
+ this.computePreferences();
}
+ if (changedProperties.has('changes')) {
+ this.changesChanged();
+ }
+ }
+
+ override updated(changedProperties: PropertyValues) {
+ if (changedProperties.has('sections')) {
+ this.sectionsChanged();
+ }
+ }
+
+ private computePreferences() {
+ if (!this.config) return;
+
+ const changes = (this.sections ?? [])
+ .map(section => section.results)
+ .flat();
this.changeTableColumns = columnNames;
this.showNumber = false;
this.visibleChangeTableColumns = this.changeTableColumns.filter(col =>
- this._isColumnEnabled(col, config, this.flagsService.enabledExperiments)
+ this._isColumnEnabled(col, this.config, changes)
);
- if (account && preferences) {
- this.showNumber = !!(
- preferences && preferences.legacycid_in_change_table
- );
- if (preferences.change_table && preferences.change_table.length > 0) {
- const prefColumns = preferences.change_table.map(column =>
+ if (this.account && this.preferences) {
+ this.showNumber = !!this.preferences?.legacycid_in_change_table;
+ if (
+ this.preferences?.change_table &&
+ this.preferences.change_table.length > 0
+ ) {
+ const prefColumns = this.preferences.change_table.map(column =>
column === 'Project' ? 'Repo' : column
);
this.visibleChangeTableColumns = prefColumns.filter(col =>
- this._isColumnEnabled(
- col,
- config,
- this.flagsService.enabledExperiments
- )
+ this._isColumnEnabled(col, this.config, changes)
);
}
}
}
/**
- * Is the column disabled by a server config or experiment? For example the
- * assignee feature might be disabled and thus the corresponding column is
- * also disabled.
- *
+ * Is the column disabled by a server config or experiment?
*/
- _isColumnEnabled(column: string, config: ServerInfo, experiments: string[]) {
+ _isColumnEnabled(
+ column: string,
+ config?: ServerInfo,
+ changes?: ChangeInfo[]
+ ) {
+ if (!columnNames.includes(column)) return false;
if (!config || !config.change) return true;
- if (column === 'Assignee') return !!config.change.enable_assignee;
- if (column === 'Comments') return experiments.includes('comments-column');
+ if (column === 'Comments')
+ return this.flagsService.isEnabled('comments-column');
+ if (column === 'Status') {
+ return (changes ?? []).every(
+ change => !showNewSubmitRequirements(this.flagsService, change)
+ );
+ }
+ if (column === ' Status ')
+ return (changes ?? []).some(change =>
+ showNewSubmitRequirements(this.flagsService, change)
+ );
return true;
}
@@ -270,12 +467,9 @@
*
* @param visibleColumns are the columns according to configs and user prefs
*/
- _computeColumns(
- section?: ChangeListSection,
- visibleColumns?: string[]
- ): string[] {
- if (!section || !visibleColumns) return [];
- const cols = [...visibleColumns];
+ private computeColumns(section?: ChangeListSection): string[] {
+ if (!section || !this.visibleChangeTableColumns) return [];
+ const cols = [...this.visibleChangeTableColumns];
const updatedIndex = cols.indexOf('Updated');
if (section.name === YOUR_TURN.name && updatedIndex !== -1) {
cols[updatedIndex] = 'Waiting';
@@ -286,20 +480,16 @@
return cols;
}
- _computeColspan(
- section?: ChangeListSection,
- visibleColumns?: string[],
- labelNames?: string[]
- ) {
- const cols = this._computeColumns(section, visibleColumns);
+ // private but used in test
+ computeColspan(section?: ChangeListSection, labelNames?: string[]) {
+ const cols = this.computeColumns(section);
if (!cols || !labelNames) return 1;
return cols.length + labelNames.length + NUMBER_FIXED_COLUMNS;
}
- _computeLabelNames(sections: ChangeListSection[]) {
- if (!sections) {
- return [];
- }
+ // private but used in test
+ computeLabelNames(sections: ChangeListSection[]) {
+ if (!sections) return [];
let labels: string[] = [];
const nonExistingLabel = function (item: string) {
return !labels.includes(item);
@@ -316,10 +506,23 @@
labels = labels.concat(currentLabels.filter(nonExistingLabel));
}
}
+ const changes = sections.map(section => section.results).flat();
+ if (
+ (changes ?? []).some(change =>
+ showNewSubmitRequirements(this.flagsService, change)
+ )
+ ) {
+ labels = (changes ?? [])
+ .map(change => getRequirements(change))
+ .flat()
+ .map(requirement => requirement.name)
+ .filter(unique);
+ }
return labels.sort();
}
- _computeLabelShortcut(labelName: string) {
+ // private but used in test
+ computeLabelShortcut(labelName: string) {
if (labelName.startsWith(LABEL_PREFIX_INVALID_PROLOG)) {
labelName = labelName.slice(LABEL_PREFIX_INVALID_PROLOG.length);
}
@@ -334,11 +537,12 @@
.slice(0, MAX_SHORTCUT_CHARS);
}
- _changesChanged(changes: ChangeInfo[]) {
- this.sections = changes ? [{results: changes}] : [];
+ private changesChanged() {
+ this.sections = this.changes ? [{results: this.changes}] : [];
}
- _processQuery(query: string) {
+ // private but used in test
+ processQuery(query: string) {
let tokens = query.split(' ');
const invalidTokens = ['limit:', 'age:', '-age:'];
tokens = tokens.filter(
@@ -348,19 +552,22 @@
return tokens.join(' ');
}
- _sectionHref(query: string) {
- return GerritNav.getUrlForSearchQuery(this._processQuery(query));
+ private sectionHref(query?: string) {
+ if (!query) return;
+ return GerritNav.getUrlForSearchQuery(this.processQuery(query));
}
/**
* Maps an index local to a particular section to the absolute index
* across all the changes on the page.
*
+ * private but used in test
+ *
* @param sectionIndex index of section
* @param localIndex index of row within section
* @return absolute index of row in the aggregate dashboard
*/
- _computeItemAbsoluteIndex(sectionIndex: number, localIndex: number) {
+ computeItemAbsoluteIndex(sectionIndex: number, localIndex: number) {
let idx = 0;
for (let i = 0; i < sectionIndex; i++) {
idx += this.sections[i].results.length;
@@ -368,97 +575,66 @@
return idx + localIndex;
}
- _computeItemSelected(
+ private computeItemSelected(
sectionIndex: number,
index: number,
- selectedIndex: number
+ selectedIndex?: number
) {
- const idx = this._computeItemAbsoluteIndex(sectionIndex, index);
+ const idx = this.computeItemAbsoluteIndex(sectionIndex, index);
return idx === selectedIndex;
}
- _computeTabIndex(
+ private computeTabIndex(
sectionIndex: number,
index: number,
- selectedIndex: number,
- isCursorMoving: boolean
+ isCursorMoving: boolean,
+ selectedIndex?: number
) {
if (isCursorMoving) return 0;
- return this._computeItemSelected(sectionIndex, index, selectedIndex)
+ return this.computeItemSelected(sectionIndex, index, selectedIndex)
? 0
: undefined;
}
- _computeItemHighlight(
- account?: AccountInfo,
- change?: ChangeInfo,
- sectionName?: string
- ) {
- if (!change || !account) return false;
- if (CLOSED_STATUS.indexOf(change.status) !== -1) return false;
- return (
- hasAttention(account, change) &&
- !isOwner(change, account) &&
- sectionName === YOUR_TURN.name
- );
- }
-
- _nextChange() {
+ private nextChange() {
this.isCursorMoving = true;
this.cursor.next();
this.isCursorMoving = false;
this.selectedIndex = this.cursor.index;
+ fire(this, 'selected-index-changed', {value: this.cursor.index});
}
- _prevChange() {
+ private prevChange() {
this.isCursorMoving = true;
this.cursor.previous();
this.isCursorMoving = false;
this.selectedIndex = this.cursor.index;
+ fire(this, 'selected-index-changed', {value: this.cursor.index});
}
- openChange() {
- const change = this._changeForIndex(this.selectedIndex);
+ private openChange() {
+ const change = this.changeForIndex(this.selectedIndex);
if (change) GerritNav.navigateToChange(change);
}
- _nextPage() {
+ private nextPage() {
fireEvent(this, 'next-page');
}
- _prevPage() {
- this.dispatchEvent(
- new CustomEvent('previous-page', {
- composed: true,
- bubbles: true,
- })
- );
+ private prevPage() {
+ fireEvent(this, 'previous-page');
}
- _toggleChangeReviewed() {
- this._toggleReviewedForIndex(this.selectedIndex);
- }
-
- _toggleReviewedForIndex(index?: number) {
- const changeEls = this._getListItems();
- if (index === undefined || index >= changeEls.length || !changeEls[index]) {
- return;
- }
-
- const changeEl = changeEls[index];
- changeEl.toggleReviewed();
- }
-
- _refreshChangeList() {
+ private refreshChangeList() {
fireReload(this);
}
- _toggleChangeStar() {
- this._toggleStarForIndex(this.selectedIndex);
+ private toggleChangeStar() {
+ this.toggleStarForIndex(this.selectedIndex);
}
- _toggleStarForIndex(index?: number) {
- const changeEls = this._getListItems();
+ private toggleStarForIndex(index?: number) {
+ const changeEls = this.getListItems();
if (index === undefined || index >= changeEls.length || !changeEls[index]) {
return;
}
@@ -468,46 +644,47 @@
if (grChangeStar) grChangeStar.toggleStar();
}
- _changeForIndex(index?: number) {
- const changeEls = this._getListItems();
+ private changeForIndex(index?: number) {
+ const changeEls = this.getListItems();
if (index !== undefined && index < changeEls.length && changeEls[index]) {
return changeEls[index].change;
}
return null;
}
- _getListItems() {
- const items = this.root?.querySelectorAll('gr-change-list-item');
+ private getListItems() {
+ const items = queryAll<GrChangeListItem>(this, 'gr-change-list-item');
return !items ? [] : Array.from(items);
}
- @observe('sections.*')
- _sectionsChanged() {
- // Flush DOM operations so that the list item elements will be loaded.
- afterNextRender(this, () => {
- this.cursor.stops = this._getListItems();
- this.cursor.moveToStart();
- if (this.selectedIndex) this.cursor.setCursorAtIndex(this.selectedIndex);
- });
+ private sectionsChanged() {
+ this.cursor.stops = this.getListItems();
+ this.cursor.moveToStart();
+ if (this.selectedIndex) this.cursor.setCursorAtIndex(this.selectedIndex);
}
- _getSpecialEmptySlot(section: DashboardSection) {
+ // private but used in test
+ getSpecialEmptySlot(section: ChangeListSection) {
if (section.isOutgoing) return 'empty-outgoing';
if (section.name === YOUR_TURN.name) return 'empty-your-turn';
return '';
}
- _isEmpty(section: DashboardSection) {
+ // private but used in test
+ isEmpty(section: ChangeListSection) {
return !section.results?.length;
}
- _computeAriaLabel(change?: ChangeInfo, sectionName?: string) {
+ private computeAriaLabel(change?: ChangeInfo, sectionName?: string) {
if (!change) return '';
return change.subject + (sectionName ? `, section: ${sectionName}` : '');
}
}
declare global {
+ interface HTMLElementEventMap {
+ 'selected-index-changed': ValueChangedEvent<number>;
+ }
interface HTMLElementTagNameMap {
'gr-change-list': GrChangeList;
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.ts
deleted file mode 100644
index c31da77..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.ts
+++ /dev/null
@@ -1,162 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-change-list-styles">
- #changeList {
- border-collapse: collapse;
- width: 100%;
- }
- .section-count-label {
- color: var(--deemphasized-text-color);
- font-family: var(--font-family);
- font-size: var(--font-size-small);
- font-weight: var(--font-weight-normal);
- line-height: var(--line-height-small);
- }
- a.section-title:hover {
- text-decoration: none;
- }
- a.section-title:hover .section-count-label {
- text-decoration: none;
- }
- a.section-title:hover .section-name {
- text-decoration: underline;
- }
- </style>
- <table id="changeList">
- <template
- is="dom-repeat"
- items="[[sections]]"
- as="changeSection"
- index-as="sectionIndex"
- >
- <template is="dom-if" if="[[changeSection.name]]">
- <tbody>
- <tr class="groupHeader">
- <td aria-hidden="true" class="leftPadding"></td>
- <td
- aria-hidden="true"
- class="star"
- hidden$="[[!showStar]]"
- hidden=""
- ></td>
- <td
- class="cell"
- colspan$="[[_computeColspan(changeSection, visibleChangeTableColumns, labelNames)]]"
- >
- <h2 class="heading-3">
- <a
- href$="[[_sectionHref(changeSection.query)]]"
- class="section-title"
- >
- <span class="section-name">[[changeSection.name]]</span>
- <span class="section-count-label"
- >[[changeSection.countLabel]]</span
- >
- </a>
- </h2>
- </td>
- </tr>
- </tbody>
- </template>
- <tbody class="groupContent">
- <template is="dom-if" if="[[_isEmpty(changeSection)]]">
- <tr class="noChanges">
- <td aria-hidden="true" class="leftPadding"></td>
- <td
- aria-hidden="[[!showStar]]"
- class="star"
- hidden$="[[!showStar]]"
- ></td>
- <td
- class="cell"
- colspan$="[[_computeColspan(changeSection, visibleChangeTableColumns, labelNames)]]"
- >
- <template
- is="dom-if"
- if="[[_getSpecialEmptySlot(changeSection)]]"
- >
- <slot name="[[_getSpecialEmptySlot(changeSection)]]"></slot>
- </template>
- <template
- is="dom-if"
- if="[[!_getSpecialEmptySlot(changeSection)]]"
- >
- No changes
- </template>
- </td>
- </tr>
- </template>
- <template is="dom-if" if="[[!_isEmpty(changeSection)]]">
- <tr class="groupTitle">
- <td aria-hidden="true" class="leftPadding"></td>
- <td
- aria-label="Star status column"
- class="star"
- hidden$="[[!showStar]]"
- hidden=""
- ></td>
- <td class="number" hidden$="[[!showNumber]]" hidden="">#</td>
- <template
- is="dom-repeat"
- items="[[_computeColumns(changeSection, visibleChangeTableColumns)]]"
- as="item"
- >
- <td class$="[[_lowerCase(item)]]">[[item]]</td>
- </template>
- <template is="dom-repeat" items="[[labelNames]]" as="labelName">
- <td class="label" title$="[[labelName]]">
- [[_computeLabelShortcut(labelName)]]
- </td>
- </template>
- <template
- is="dom-repeat"
- items="[[_dynamicHeaderEndpoints]]"
- as="pluginHeader"
- >
- <td class="endpoint">
- <gr-endpoint-decorator name$="[[pluginHeader]]">
- </gr-endpoint-decorator>
- </td>
- </template>
- </tr>
- </template>
- <template is="dom-repeat" items="[[changeSection.results]]" as="change">
- <gr-change-list-item
- account="[[account]]"
- selected$="[[_computeItemSelected(sectionIndex, index, selectedIndex)]]"
- highlight$="[[_computeItemHighlight(account, change, changeSection.name)]]"
- change="[[change]]"
- config="[[_config]]"
- section-name="[[changeSection.name]]"
- visible-change-table-columns="[[_computeColumns(changeSection, visibleChangeTableColumns)]]"
- show-number="[[showNumber]]"
- show-star="[[showStar]]"
- tabindex$="[[_computeTabIndex(sectionIndex, index, selectedIndex, isCursorMoving)]]"
- label-names="[[labelNames]]"
- aria-label$="[[_computeAriaLabel(change, changeSection.name)]]"
- ></gr-change-list-item>
- </template>
- </tbody>
- </template>
- </table>
-`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
deleted file mode 100644
index de1a0a2..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.js
+++ /dev/null
@@ -1,517 +0,0 @@
-/**
- * @license
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-change-list.js';
-import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {mockPromise} from '../../../test/test-utils.js';
-import {YOUR_TURN} from '../../core/gr-navigation/gr-navigation.js';
-
-const basicFixture = fixtureFromElement('gr-change-list');
-
-suite('gr-change-list basic tests', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- });
-
- suite('test show change number not logged in', () => {
- setup(() => {
- element = basicFixture.instantiate();
- element.account = undefined;
- element.preferences = undefined;
- element._config = {};
- });
-
- test('show number disabled', () => {
- assert.isFalse(element.showNumber);
- });
- });
-
- suite('test show change number preference enabled', () => {
- setup(() => {
- element = basicFixture.instantiate();
- element.preferences = {
- legacycid_in_change_table: true,
- time_format: 'HHMM_12',
- change_table: [],
- };
- element.account = {_account_id: 1001};
- element._config = {};
- flush();
- });
-
- test('show number enabled', () => {
- assert.isTrue(element.showNumber);
- });
- });
-
- suite('test show change number preference disabled', () => {
- setup(() => {
- element = basicFixture.instantiate();
- // legacycid_in_change_table is not set when false.
- element.preferences = {
- time_format: 'HHMM_12',
- change_table: [],
- };
- element.account = {_account_id: 1001};
- element._config = {};
- flush();
- });
-
- test('show number disabled', () => {
- assert.isFalse(element.showNumber);
- });
- });
-
- test('computed fields', () => {
- assert.equal(element._computeLabelNames(
- [{results: [{_number: 0, labels: {}}]}]).length, 0);
- assert.equal(element._computeLabelNames([
- {results: [
- {_number: 0, labels: {Verified: {approved: {}}}},
- {
- _number: 1,
- labels: {
- 'Verified': {approved: {}},
- 'Code-Review': {approved: {}},
- },
- },
- {
- _number: 2,
- labels: {
- 'Verified': {approved: {}},
- 'Library-Compliance': {approved: {}},
- },
- },
- ]},
- ]).length, 3);
-
- assert.equal(element._computeLabelShortcut('Code-Review'), 'CR');
- assert.equal(element._computeLabelShortcut('Verified'), 'V');
- assert.equal(element._computeLabelShortcut('Library-Compliance'), 'LC');
- assert.equal(element._computeLabelShortcut('PolyGerrit-Review'), 'PR');
- assert.equal(element._computeLabelShortcut('polygerrit-review'), 'PR');
- assert.equal(element._computeLabelShortcut(
- 'Invalid-Prolog-Rules-Label-Name--Verified'), 'V');
- assert.equal(element._computeLabelShortcut(
- 'Some-Special-Label-7'), 'SSL7');
- assert.equal(element._computeLabelShortcut('--Too----many----dashes---'),
- 'TMD');
- assert.equal(element._computeLabelShortcut(
- 'Really-rather-entirely-too-long-of-a-label-name'), 'RRETL');
- });
-
- test('colspans', () => {
- element.sections = [
- {results: [{}]},
- ];
- flush();
- const tdItemCount = element.root.querySelectorAll(
- 'td').length;
-
- const changeTableColumns = [];
- const labelNames = [];
- assert.equal(tdItemCount, element._computeColspan(
- {}, changeTableColumns, labelNames));
- });
-
- test('keyboard shortcuts', async () => {
- sinon.stub(element, '_computeLabelNames');
- element.sections = [
- {results: new Array(1)},
- {results: new Array(2)},
- ];
- element.selectedIndex = 0;
- element.changes = [
- {_number: 0},
- {_number: 1},
- {_number: 2},
- ];
- await flush();
- const promise = mockPromise();
- afterNextRender(element, () => {
- promise.resolve();
- });
- await promise;
- const elementItems = element.root.querySelectorAll(
- 'gr-change-list-item');
- assert.equal(elementItems.length, 3);
-
- assert.isTrue(elementItems[0].hasAttribute('selected'));
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
- assert.equal(element.selectedIndex, 1);
- assert.isTrue(elementItems[1].hasAttribute('selected'));
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
- assert.equal(element.selectedIndex, 2);
- assert.isTrue(elementItems[2].hasAttribute('selected'));
-
- const navStub = sinon.stub(GerritNav, 'navigateToChange');
- assert.equal(element.selectedIndex, 2);
- MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
- assert.deepEqual(navStub.lastCall.args[0], {_number: 2},
- 'Should navigate to /c/2/');
-
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- assert.equal(element.selectedIndex, 1);
- MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
- assert.deepEqual(navStub.lastCall.args[0], {_number: 1},
- 'Should navigate to /c/1/');
-
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- assert.equal(element.selectedIndex, 0);
- });
-
- test('no changes', () => {
- element.changes = [];
- flush();
- const listItems = element.root.querySelectorAll(
- 'gr-change-list-item');
- assert.equal(listItems.length, 0);
- const noChangesMsg =
- element.root.querySelector('.noChanges');
- assert.ok(noChangesMsg);
- });
-
- test('empty sections', () => {
- element.sections = [{results: []}, {results: []}];
- flush();
- const listItems = element.root.querySelectorAll(
- 'gr-change-list-item');
- assert.equal(listItems.length, 0);
- const noChangesMsg = element.root.querySelectorAll(
- '.noChanges');
- assert.equal(noChangesMsg.length, 2);
- });
-
- suite('empty section', () => {
- test('not shown on empty non-outgoing sections', () => {
- const section = {results: []};
- assert.isTrue(element._isEmpty(section));
- assert.equal(element._getSpecialEmptySlot(section), '');
- });
-
- test('shown on empty outgoing sections', () => {
- const section = {results: [], isOutgoing: true};
- assert.isTrue(element._isEmpty(section));
- assert.equal(element._getSpecialEmptySlot(section), 'empty-outgoing');
- });
-
- test('shown on empty outgoing sections', () => {
- const section = {results: [], name: YOUR_TURN.name};
- assert.isTrue(element._isEmpty(section));
- assert.equal(element._getSpecialEmptySlot(section), 'empty-your-turn');
- });
-
- test('not shown on non-empty outgoing sections', () => {
- const section = {isOutgoing: true, results: [
- {_number: 0, labels: {Verified: {approved: {}}}}]};
- assert.isFalse(element._isEmpty(section));
- });
- });
-
- suite('empty column preference', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- element.sections = [
- {results: [{}]},
- ];
- element.account = {_account_id: 1001};
- element.preferences = {
- legacycid_in_change_table: true,
- time_format: 'HHMM_12',
- change_table: [],
- };
- element._config = {};
- flush();
- });
-
- test('show number enabled', () => {
- assert.isTrue(element.showNumber);
- });
-
- test('all columns visible', () => {
- for (const column of element.changeTableColumns) {
- const elementClass = '.' + element._lowerCase(column);
- assert.isFalse(element.shadowRoot
- .querySelector(elementClass).hidden);
- }
- });
- });
-
- suite('full column preference', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- element.sections = [
- {results: [{}]},
- ];
- element.account = {_account_id: 1001};
- element.preferences = {
- legacycid_in_change_table: true,
- time_format: 'HHMM_12',
- change_table: [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Reviewers',
- 'Comments',
- 'Repo',
- 'Branch',
- 'Updated',
- 'Size',
- ],
- };
- element._config = {};
- flush();
- });
-
- test('all columns visible', () => {
- for (const column of element.changeTableColumns) {
- const elementClass = '.' + element._lowerCase(column);
- assert.isFalse(element.shadowRoot
- .querySelector(elementClass).hidden);
- }
- });
- });
-
- suite('partial column preference', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- element.sections = [
- {results: [{}]},
- ];
- element.account = {_account_id: 1001};
- element.preferences = {
- legacycid_in_change_table: true,
- time_format: 'HHMM_12',
- change_table: [
- 'Subject',
- 'Status',
- 'Owner',
- 'Assignee',
- 'Reviewers',
- 'Comments',
- 'Branch',
- 'Updated',
- 'Size',
- ],
- };
- element._config = {};
- flush();
- });
-
- test('all columns except repo visible', () => {
- for (const column of element.changeTableColumns) {
- const elementClass = '.' + column.toLowerCase();
- if (column === 'Repo') {
- assert.isNotOk(element.shadowRoot.querySelector(elementClass));
- } else {
- assert.isOk(element.shadowRoot.querySelector(elementClass));
- }
- }
- });
- });
-
- suite('random column does not exist', () => {
- let element;
-
- /* This would only exist if somebody manually updated the config
- file. */
- setup(() => {
- element = basicFixture.instantiate();
- element.account = {_account_id: 1001};
- element.preferences = {
- legacycid_in_change_table: true,
- time_format: 'HHMM_12',
- change_table: [
- 'Bad',
- ],
- };
- flush();
- });
-
- test('bad column does not exist', () => {
- const elementClass = '.bad';
- assert.isNotOk(element.shadowRoot
- .querySelector(elementClass));
- });
- });
-
- suite('dashboard queries', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- });
-
- teardown(() => { sinon.restore(); });
-
- test('query without age and limit unchanged', () => {
- const query = 'status:closed owner:me';
- assert.deepEqual(element._processQuery(query), query);
- });
-
- test('query with age and limit', () => {
- const query = 'status:closed age:1week limit:10 owner:me';
- const expectedQuery = 'status:closed owner:me';
- assert.deepEqual(element._processQuery(query), expectedQuery);
- });
-
- test('query with age', () => {
- const query = 'status:closed age:1week owner:me';
- const expectedQuery = 'status:closed owner:me';
- assert.deepEqual(element._processQuery(query), expectedQuery);
- });
-
- test('query with limit', () => {
- const query = 'status:closed limit:10 owner:me';
- const expectedQuery = 'status:closed owner:me';
- assert.deepEqual(element._processQuery(query), expectedQuery);
- });
-
- test('query with age as value and not key', () => {
- const query = 'status:closed random:age';
- const expectedQuery = 'status:closed random:age';
- assert.deepEqual(element._processQuery(query), expectedQuery);
- });
-
- test('query with limit as value and not key', () => {
- const query = 'status:closed random:limit';
- const expectedQuery = 'status:closed random:limit';
- assert.deepEqual(element._processQuery(query), expectedQuery);
- });
-
- test('query with -age key', () => {
- const query = 'status:closed -age:1week';
- const expectedQuery = 'status:closed';
- assert.deepEqual(element._processQuery(query), expectedQuery);
- });
- });
-
- suite('gr-change-list sections', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- });
-
- test('keyboard shortcuts', async () => {
- element.selectedIndex = 0;
- element.sections = [
- {
- results: [
- {_number: 0},
- {_number: 1},
- {_number: 2},
- ],
- },
- {
- results: [
- {_number: 3},
- {_number: 4},
- {_number: 5},
- ],
- },
- {
- results: [
- {_number: 6},
- {_number: 7},
- {_number: 8},
- ],
- },
- ];
- await flush();
- const promise = mockPromise();
- afterNextRender(element, () => {
- promise.resolve();
- });
- await promise;
- const elementItems = element.root.querySelectorAll(
- 'gr-change-list-item');
- assert.equal(elementItems.length, 9);
-
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
- assert.equal(element.selectedIndex, 1);
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
-
- const navStub = sinon.stub(GerritNav, 'navigateToChange');
- assert.equal(element.selectedIndex, 2);
-
- MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'Enter');
- assert.deepEqual(navStub.lastCall.args[0], {_number: 2},
- 'Should navigate to /c/2/');
-
- MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
- assert.equal(element.selectedIndex, 1);
- MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'Enter');
- assert.deepEqual(navStub.lastCall.args[0], {_number: 1},
- 'Should navigate to /c/1/');
-
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
- MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
- assert.equal(element.selectedIndex, 4);
- MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'Enter');
- assert.deepEqual(navStub.lastCall.args[0], {_number: 4},
- 'Should navigate to /c/4/');
-
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
- const change = element._changeForIndex(element.selectedIndex);
- assert.equal(change.reviewed, true,
- 'Should mark change as reviewed');
- MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
- assert.equal(change.reviewed, false,
- 'Should mark change as unreviewed');
- });
-
- test('_computeItemHighlight gives false for null account', () => {
- assert.isFalse(
- element._computeItemHighlight(null, {assignee: {_account_id: 42}}));
- });
-
- test('_computeItemAbsoluteIndex', () => {
- sinon.stub(element, '_computeLabelNames');
- element.sections = [
- {results: new Array(1)},
- {results: new Array(2)},
- {results: new Array(3)},
- ];
-
- assert.equal(element._computeItemAbsoluteIndex(0, 0), 0);
- // Out of range but no matter.
- assert.equal(element._computeItemAbsoluteIndex(0, 1), 1);
-
- assert.equal(element._computeItemAbsoluteIndex(1, 0), 1);
- assert.equal(element._computeItemAbsoluteIndex(1, 1), 2);
- assert.equal(element._computeItemAbsoluteIndex(1, 2), 3);
- assert.equal(element._computeItemAbsoluteIndex(2, 0), 3);
- assert.equal(element._computeItemAbsoluteIndex(3, 0), 6);
- });
- });
-});
-
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
new file mode 100644
index 0000000..50708c0
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
@@ -0,0 +1,575 @@
+/**
+ * @license
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../../../test/common-test-setup-karma';
+import './gr-change-list';
+import {GrChangeList} from './gr-change-list';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {
+ pressKey,
+ query,
+ queryAll,
+ queryAndAssert,
+ stubFlags,
+} from '../../../test/test-utils';
+import {YOUR_TURN} from '../../core/gr-navigation/gr-navigation';
+import {Key} from '../../../utils/dom-util';
+import {TimeFormat} from '../../../constants/constants';
+import {AccountId, NumericChangeId} from '../../../types/common';
+import {
+ createChange,
+ createServerInfo,
+} from '../../../test/test-data-generators';
+import {GrChangeListItem} from '../gr-change-list-item/gr-change-list-item';
+
+const basicFixture = fixtureFromElement('gr-change-list');
+
+suite('gr-change-list basic tests', () => {
+ let element: GrChangeList;
+
+ setup(() => {
+ element = basicFixture.instantiate();
+ });
+
+ suite('test show change number not logged in', () => {
+ setup(async () => {
+ element = basicFixture.instantiate();
+ element.account = undefined;
+ element.preferences = undefined;
+ element.config = createServerInfo();
+ await element.updateComplete;
+ });
+
+ test('show number disabled', () => {
+ assert.isFalse(element.showNumber);
+ });
+ });
+
+ suite('test show change number preference enabled', () => {
+ setup(async () => {
+ element = basicFixture.instantiate();
+ element.preferences = {
+ legacycid_in_change_table: true,
+ time_format: TimeFormat.HHMM_12,
+ change_table: [],
+ };
+ element.account = {_account_id: 1001 as AccountId};
+ element.config = createServerInfo();
+ await element.updateComplete;
+ });
+
+ test('show number enabled', () => {
+ assert.isTrue(element.showNumber);
+ });
+ });
+
+ suite('test show change number preference disabled', () => {
+ setup(async () => {
+ element = basicFixture.instantiate();
+ // legacycid_in_change_table is not set when false.
+ element.preferences = {
+ time_format: TimeFormat.HHMM_12,
+ change_table: [],
+ };
+ element.account = {_account_id: 1001 as AccountId};
+ element.config = createServerInfo();
+ await element.updateComplete;
+ });
+
+ test('show number disabled', () => {
+ assert.isFalse(element.showNumber);
+ });
+ });
+
+ test('computed fields', () => {
+ assert.equal(
+ element.computeLabelNames([
+ {
+ results: [
+ {...createChange(), _number: 0 as NumericChangeId, labels: {}},
+ ],
+ },
+ ]).length,
+ 0
+ );
+ assert.equal(
+ element.computeLabelNames([
+ {
+ results: [
+ {
+ ...createChange(),
+ _number: 0 as NumericChangeId,
+ labels: {Verified: {approved: {}}},
+ },
+ {
+ ...createChange(),
+ _number: 1 as NumericChangeId,
+ labels: {
+ Verified: {approved: {}},
+ 'Code-Review': {approved: {}},
+ },
+ },
+ {
+ ...createChange(),
+ _number: 2 as NumericChangeId,
+ labels: {
+ Verified: {approved: {}},
+ 'Library-Compliance': {approved: {}},
+ },
+ },
+ ],
+ },
+ ]).length,
+ 3
+ );
+
+ assert.equal(element.computeLabelShortcut('Code-Review'), 'CR');
+ assert.equal(element.computeLabelShortcut('Verified'), 'V');
+ assert.equal(element.computeLabelShortcut('Library-Compliance'), 'LC');
+ assert.equal(element.computeLabelShortcut('PolyGerrit-Review'), 'PR');
+ assert.equal(element.computeLabelShortcut('polygerrit-review'), 'PR');
+ assert.equal(
+ element.computeLabelShortcut('Invalid-Prolog-Rules-Label-Name--Verified'),
+ 'V'
+ );
+ assert.equal(element.computeLabelShortcut('Some-Special-Label-7'), 'SSL7');
+ assert.equal(
+ element.computeLabelShortcut('--Too----many----dashes---'),
+ 'TMD'
+ );
+ assert.equal(
+ element.computeLabelShortcut(
+ 'Really-rather-entirely-too-long-of-a-label-name'
+ ),
+ 'RRETL'
+ );
+ });
+
+ test('colspans', async () => {
+ element.sections = [{results: [{...createChange()}]}];
+ await element.updateComplete;
+ const tdItemCount = queryAll<HTMLTableElement>(element, 'td').length;
+
+ element.visibleChangeTableColumns = [];
+ const labelNames: string[] | undefined = [];
+ assert.equal(
+ tdItemCount,
+ element.computeColspan({results: [{...createChange()}]}, labelNames)
+ );
+ });
+
+ test('keyboard shortcuts', async () => {
+ sinon.stub(element, 'computeLabelNames');
+ element.sections = [{results: new Array(1)}, {results: new Array(2)}];
+ element.selectedIndex = 0;
+ element.changes = [
+ {...createChange(), _number: 0 as NumericChangeId},
+ {...createChange(), _number: 1 as NumericChangeId},
+ {...createChange(), _number: 2 as NumericChangeId},
+ ];
+ await element.updateComplete;
+ const elementItems = queryAll<GrChangeListItem>(
+ element,
+ 'gr-change-list-item'
+ );
+ assert.equal(elementItems.length, 3);
+
+ assert.isTrue(elementItems[0].hasAttribute('selected'));
+ pressKey(element, 'j');
+ await element.updateComplete;
+ assert.equal(element.selectedIndex, 1);
+ assert.isTrue(elementItems[1].hasAttribute('selected'));
+ pressKey(element, 'j');
+ await element.updateComplete;
+ assert.equal(element.selectedIndex, 2);
+ assert.isTrue(elementItems[2].hasAttribute('selected'));
+
+ const navStub = sinon.stub(GerritNav, 'navigateToChange');
+ assert.equal(element.selectedIndex, 2);
+ pressKey(element, Key.ENTER);
+ await element.updateComplete;
+ assert.deepEqual(
+ navStub.lastCall.args[0],
+ {...createChange(), _number: 2 as NumericChangeId},
+ 'Should navigate to /c/2/'
+ );
+
+ pressKey(element, 'k');
+ await element.updateComplete;
+ assert.equal(element.selectedIndex, 1);
+ pressKey(element, Key.ENTER);
+ await element.updateComplete;
+ assert.deepEqual(
+ navStub.lastCall.args[0],
+ {...createChange(), _number: 1 as NumericChangeId},
+ 'Should navigate to /c/1/'
+ );
+
+ pressKey(element, 'k');
+ pressKey(element, 'k');
+ pressKey(element, 'k');
+ assert.equal(element.selectedIndex, 0);
+ });
+
+ test('no changes', async () => {
+ element.changes = [];
+ await element.updateComplete;
+ const listItems = queryAll<GrChangeListItem>(
+ element,
+ 'gr-change-list-item'
+ );
+ assert.equal(listItems.length, 0);
+ const noChangesMsg = queryAndAssert<HTMLTableRowElement>(
+ element,
+ '.noChanges'
+ );
+ assert.ok(noChangesMsg);
+ });
+
+ test('empty sections', async () => {
+ element.sections = [{results: []}, {results: []}];
+ await element.updateComplete;
+ const listItems = queryAll<GrChangeListItem>(
+ element,
+ 'gr-change-list-item'
+ );
+ assert.equal(listItems.length, 0);
+ const noChangesMsg = queryAll<HTMLTableRowElement>(element, '.noChanges');
+ assert.equal(noChangesMsg.length, 2);
+ });
+
+ suite('empty section', () => {
+ test('not shown on empty non-outgoing sections', () => {
+ const section = {name: 'test', query: 'test', results: []};
+ assert.isTrue(element.isEmpty(section));
+ assert.equal(element.getSpecialEmptySlot(section), '');
+ });
+
+ test('shown on empty outgoing sections', () => {
+ const section = {
+ name: 'test',
+ query: 'test',
+ results: [],
+ isOutgoing: true,
+ };
+ assert.isTrue(element.isEmpty(section));
+ assert.equal(element.getSpecialEmptySlot(section), 'empty-outgoing');
+ });
+
+ test('shown on empty outgoing sections', () => {
+ const section = {name: YOUR_TURN.name, query: 'test', results: []};
+ assert.isTrue(element.isEmpty(section));
+ assert.equal(element.getSpecialEmptySlot(section), 'empty-your-turn');
+ });
+
+ test('not shown on non-empty outgoing sections', () => {
+ const section = {
+ name: 'test',
+ query: 'test',
+ isOutgoing: true,
+ results: [
+ {
+ ...createChange(),
+ _number: 0 as NumericChangeId,
+ labels: {Verified: {approved: {}}},
+ },
+ ],
+ };
+ assert.isFalse(element.isEmpty(section));
+ });
+ });
+
+ suite('empty column preference', () => {
+ let element: GrChangeList;
+
+ setup(async () => {
+ stubFlags('isEnabled').returns(true);
+ element = basicFixture.instantiate();
+ element.sections = [{results: [{...createChange()}]}];
+ element.account = {_account_id: 1001 as AccountId};
+ element.preferences = {
+ legacycid_in_change_table: true,
+ time_format: TimeFormat.HHMM_12,
+ change_table: [],
+ };
+ element.config = createServerInfo();
+ await element.updateComplete;
+ });
+
+ test('show number enabled', () => {
+ assert.isTrue(element.showNumber);
+ });
+
+ test('all columns visible', () => {
+ for (const column of element.changeTableColumns!) {
+ const elementClass = '.' + column.trim().toLowerCase();
+ assert.isFalse(
+ queryAndAssert<HTMLElement>(element, elementClass)!.hidden
+ );
+ }
+ });
+ });
+
+ suite('full column preference', () => {
+ let element: GrChangeList;
+
+ setup(async () => {
+ stubFlags('isEnabled').returns(true);
+ element = basicFixture.instantiate();
+ element.sections = [{results: [{...createChange()}]}];
+ element.account = {_account_id: 1001 as AccountId};
+ element.preferences = {
+ legacycid_in_change_table: true,
+ time_format: TimeFormat.HHMM_12,
+ change_table: [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ 'Reviewers',
+ 'Comments',
+ 'Repo',
+ 'Branch',
+ 'Updated',
+ 'Size',
+ ' Status ',
+ ],
+ };
+ element.config = createServerInfo();
+ await element.updateComplete;
+ });
+
+ test('all columns visible', () => {
+ for (const column of element.changeTableColumns!) {
+ const elementClass = '.' + column.trim().toLowerCase();
+ assert.isFalse(
+ queryAndAssert<HTMLElement>(element, elementClass).hidden
+ );
+ }
+ });
+ });
+
+ suite('partial column preference', () => {
+ let element: GrChangeList;
+
+ setup(async () => {
+ stubFlags('isEnabled').returns(true);
+ element = basicFixture.instantiate();
+ element.sections = [{results: [{...createChange()}]}];
+ element.account = {_account_id: 1001 as AccountId};
+ element.preferences = {
+ legacycid_in_change_table: true,
+ time_format: TimeFormat.HHMM_12,
+ change_table: [
+ 'Subject',
+ 'Status',
+ 'Owner',
+ 'Reviewers',
+ 'Comments',
+ 'Branch',
+ 'Updated',
+ 'Size',
+ ' Status ',
+ ],
+ };
+ element.config = createServerInfo();
+ await element.updateComplete;
+ });
+
+ test('all columns except repo visible', () => {
+ for (const column of element.changeTableColumns!) {
+ const elementClass = '.' + column.trim().toLowerCase();
+ if (column === 'Repo') {
+ assert.isNotOk(query<HTMLElement>(element, elementClass));
+ } else {
+ assert.isOk(queryAndAssert<HTMLElement>(element, elementClass));
+ }
+ }
+ });
+ });
+
+ test('obsolete column in preferences not visible', () => {
+ assert.isTrue(element._isColumnEnabled('Subject'));
+ assert.isFalse(element._isColumnEnabled('Assignee'));
+ });
+
+ suite('random column does not exist', () => {
+ let element: GrChangeList;
+
+ /* This would only exist if somebody manually updated the config
+ file. */
+ setup(async () => {
+ element = basicFixture.instantiate();
+ element.account = {_account_id: 1001 as AccountId};
+ element.preferences = {
+ legacycid_in_change_table: true,
+ time_format: TimeFormat.HHMM_12,
+ change_table: ['Bad'],
+ };
+ await element.updateComplete;
+ });
+
+ test('bad column does not exist', () => {
+ assert.isNotOk(query<HTMLElement>(element, '.bad'));
+ });
+ });
+
+ suite('dashboard queries', () => {
+ let element: GrChangeList;
+
+ setup(() => {
+ element = basicFixture.instantiate();
+ });
+
+ teardown(() => {
+ sinon.restore();
+ });
+
+ test('query without age and limit unchanged', () => {
+ const query = 'status:closed owner:me';
+ assert.deepEqual(element.processQuery(query), query);
+ });
+
+ test('query with age and limit', () => {
+ const query = 'status:closed age:1week limit:10 owner:me';
+ const expectedQuery = 'status:closed owner:me';
+ assert.deepEqual(element.processQuery(query), expectedQuery);
+ });
+
+ test('query with age', () => {
+ const query = 'status:closed age:1week owner:me';
+ const expectedQuery = 'status:closed owner:me';
+ assert.deepEqual(element.processQuery(query), expectedQuery);
+ });
+
+ test('query with limit', () => {
+ const query = 'status:closed limit:10 owner:me';
+ const expectedQuery = 'status:closed owner:me';
+ assert.deepEqual(element.processQuery(query), expectedQuery);
+ });
+
+ test('query with age as value and not key', () => {
+ const query = 'status:closed random:age';
+ const expectedQuery = 'status:closed random:age';
+ assert.deepEqual(element.processQuery(query), expectedQuery);
+ });
+
+ test('query with limit as value and not key', () => {
+ const query = 'status:closed random:limit';
+ const expectedQuery = 'status:closed random:limit';
+ assert.deepEqual(element.processQuery(query), expectedQuery);
+ });
+
+ test('query with -age key', () => {
+ const query = 'status:closed -age:1week';
+ const expectedQuery = 'status:closed';
+ assert.deepEqual(element.processQuery(query), expectedQuery);
+ });
+ });
+
+ suite('gr-change-list sections', () => {
+ let element: GrChangeList;
+
+ setup(() => {
+ element = basicFixture.instantiate();
+ });
+
+ test('keyboard shortcuts', async () => {
+ element.selectedIndex = 0;
+ element.sections = [
+ {
+ results: [
+ {...createChange(), _number: 0 as NumericChangeId},
+ {...createChange(), _number: 1 as NumericChangeId},
+ {...createChange(), _number: 2 as NumericChangeId},
+ ],
+ },
+ {
+ results: [
+ {...createChange(), _number: 3 as NumericChangeId},
+ {...createChange(), _number: 4 as NumericChangeId},
+ {...createChange(), _number: 5 as NumericChangeId},
+ ],
+ },
+ {
+ results: [
+ {...createChange(), _number: 6 as NumericChangeId},
+ {...createChange(), _number: 7 as NumericChangeId},
+ {...createChange(), _number: 8 as NumericChangeId},
+ ],
+ },
+ ];
+ await element.updateComplete;
+ const elementItems = queryAll<GrChangeListItem>(
+ element,
+ 'gr-change-list-item'
+ );
+ assert.equal(elementItems.length, 9);
+
+ pressKey(element, 'j');
+ assert.equal(element.selectedIndex, 1);
+ pressKey(element, 'j');
+
+ const navStub = sinon.stub(GerritNav, 'navigateToChange');
+ assert.equal(element.selectedIndex, 2);
+
+ pressKey(element, Key.ENTER);
+ assert.deepEqual(
+ navStub.lastCall.args[0],
+ {...createChange(), _number: 2 as NumericChangeId},
+ 'Should navigate to /c/2/'
+ );
+
+ pressKey(element, 'k');
+ assert.equal(element.selectedIndex, 1);
+ pressKey(element, Key.ENTER);
+ assert.deepEqual(
+ navStub.lastCall.args[0],
+ {...createChange(), _number: 1 as NumericChangeId},
+ 'Should navigate to /c/1/'
+ );
+
+ pressKey(element, 'j');
+ pressKey(element, 'j');
+ pressKey(element, 'j');
+ assert.equal(element.selectedIndex, 4);
+ pressKey(element, Key.ENTER);
+ assert.deepEqual(
+ navStub.lastCall.args[0],
+ {...createChange(), _number: 4 as NumericChangeId},
+ 'Should navigate to /c/4/'
+ );
+ });
+
+ test('computeItemAbsoluteIndex', () => {
+ sinon.stub(element, 'computeLabelNames');
+ element.sections = [
+ {results: new Array(1)},
+ {results: new Array(2)},
+ {results: new Array(3)},
+ ];
+
+ assert.equal(element.computeItemAbsoluteIndex(0, 0), 0);
+ // Out of range but no matter.
+ assert.equal(element.computeItemAbsoluteIndex(0, 1), 1);
+
+ assert.equal(element.computeItemAbsoluteIndex(1, 0), 1);
+ assert.equal(element.computeItemAbsoluteIndex(1, 1), 2);
+ assert.equal(element.computeItemAbsoluteIndex(1, 2), 3);
+ assert.equal(element.computeItemAbsoluteIndex(2, 0), 3);
+ assert.equal(element.computeItemAbsoluteIndex(3, 0), 6);
+ });
+ });
+});
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 9eca3bc..1b04268 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
@@ -31,7 +31,7 @@
UserDashboard,
YOUR_TURN,
} from '../../core/gr-navigation/gr-navigation';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {changeIsOpen} from '../../../utils/change-util';
import {parseDate} from '../../../utils/date-util';
import {customElement, observe, property} from '@polymer/decorators';
@@ -115,9 +115,9 @@
@property({type: Number})
_selectedChangeIndex?: number;
- private reporting = appContext.reportingService;
+ private reporting = getAppContext().reportingService;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
private lastVisibleTimestampMs = 0;
@@ -225,19 +225,13 @@
const {project, dashboard, title, user, sections} = params;
const dashboardPromise: Promise<UserDashboard | undefined> = project
? this._getProjectDashboard(project, dashboard)
- : this.restApiService
- .getConfig()
- .then(config =>
- Promise.resolve(
- GerritNav.getUserDashboard(
- user,
- sections,
- title || this._computeTitle(user),
- config
- )
- )
- );
-
+ : Promise.resolve(
+ GerritNav.getUserDashboard(
+ user,
+ sections,
+ title || this._computeTitle(user)
+ )
+ );
// Checking `this.account` to make sure that the user is logged in.
// Otherwise sending a query for 'owner:self' will result in an error.
const checkForNewUser = !project && !!this.account && user === 'self';
@@ -456,12 +450,8 @@
this.$.commandsDialog.open();
}
- /**
- * Returns `this` as the visibility observer target for the keyboard shortcut
- * mixin to decide whether shortcuts should be enabled or not.
- */
- _computeObserverTarget() {
- return this;
+ _handleSelectedIndexChanged(e: CustomEvent) {
+ this._selectedChangeIndex = Number(e.detail.value);
}
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts
index a55befb..84cf6d9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.ts
@@ -78,10 +78,10 @@
show-star=""
account="[[account]]"
preferences="[[preferences]]"
- selected-index="{{_selectedChangeIndex}}"
+ selected-index="[[_selectedChangeIndex]]"
sections="[[_results]]"
+ on-selected-index-changed="_handleSelectedIndexChanged"
on-toggle-star="_handleToggleStar"
- observer-target="[[_computeObserverTarget()]]"
>
<div id="emptyOutgoing" slot="empty-outgoing">
<template is="dom-if" if="[[_showNewUserHelp]]">
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts
index d8949ed..d82ff7f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.ts
@@ -18,7 +18,7 @@
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {RepoName} from '../../../types/common';
import {WebLinkInfo} from '../../../types/diff';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {sharedStyles} from '../../../styles/shared-styles';
import {fontStyles} from '../../../styles/gr-font-styles';
import {dashboardHeaderStyles} from '../../../styles/dashboard-header-styles';
@@ -36,7 +36,7 @@
@property({type: Array})
_webLinks: WebLinkInfo[] = [];
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
static override get styles() {
return [
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
index 50de7b9..7265a7f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
@@ -22,7 +22,7 @@
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {AccountDetailInfo, AccountId} from '../../../types/common';
import {getDisplayName} from '../../../utils/display-name-util';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {dashboardHeaderStyles} from '../../../styles/dashboard-header-styles';
import {sharedStyles} from '../../../styles/shared-styles';
import {fontStyles} from '../../../styles/gr-font-styles';
@@ -46,7 +46,7 @@
@property({type: String})
_status = '';
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
static override get styles() {
return [
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 b10c17e..0c07abb 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
@@ -33,7 +33,7 @@
import {htmlTemplate} from './gr-change-actions_html';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {CURRENT} from '../../../utils/patch-set-util';
import {
changeIsOpen,
@@ -99,7 +99,6 @@
getVotingRange,
StandardLabels,
} from '../../../utils/label-util';
-import {CommentThread} from '../../../utils/comment-util';
import {ShowAlertEventDetail} from '../../../types/events';
import {
ActionPriority,
@@ -115,7 +114,7 @@
const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.';
const ERR_REVISION_ACTIONS = 'Couldn’t load revision actions.';
-enum LabelStatus {
+export enum LabelStatus {
/**
* This label provides what is necessary for submission.
*/
@@ -382,11 +381,12 @@
RevisionActions = RevisionActions;
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
- private readonly jsAPI = appContext.jsApiService;
+ // Accessed in tests
+ readonly jsAPI = getAppContext().jsApiService;
- private readonly changeService = appContext.changeService;
+ private readonly changeModel = getAppContext().changeModel;
@property({type: Object})
change?: ChangeViewChangeInfo;
@@ -448,9 +448,6 @@
@property({type: String})
_actionLoadingMessage = '';
- @property({type: Array})
- commentThreads: CommentThread[] = [];
-
@property({
type: Array,
computed:
@@ -507,14 +504,6 @@
},
{
type: ActionType.CHANGE,
- key: ChangeActions.IGNORE,
- },
- {
- type: ActionType.CHANGE,
- key: ChangeActions.UNIGNORE,
- },
- {
- type: ActionType.CHANGE,
key: ChangeActions.REVIEWED,
},
{
@@ -563,7 +552,7 @@
@property({type: Object})
_config?: ServerInfo;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
@@ -1727,7 +1716,7 @@
new Error('Properties change and changeNum must be set.')
);
}
- return this.changeService.fetchChangeUpdates(change).then(result => {
+ return this.changeModel.fetchChangeUpdates(change).then(result => {
if (!result.isLatest) {
this.dispatchEvent(
new CustomEvent<ShowAlertEventDetail>('show-alert', {
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.ts
index d21c29f..17ca7cf 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.ts
@@ -213,11 +213,9 @@
<gr-confirm-submit-dialog
id="confirmSubmitDialog"
class="confirmDialog"
- change="[[change]]"
action="[[_revisionSubmitAction]]"
on-cancel="_handleConfirmDialogCancel"
on-confirm="_handleSubmitConfirm"
- comment-threads="[[commentThreads]]"
hidden=""
></gr-confirm-submit-dialog>
<gr-dialog
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 26e2fb4..2abc2bc 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
@@ -60,7 +60,6 @@
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {UIActionInfo} from '../../shared/gr-js-api-interface/gr-change-actions-js-api';
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
-import {appContext} from '../../../services/app-context';
const basicFixture = fixtureFromElement('gr-change-actions');
@@ -590,9 +589,7 @@
test('_setReviewOnRevert', () => {
const review = {labels: {Foo: 1, 'Bar-Baz': -2}};
const changeId = 1234 as NumericChangeId;
- sinon
- .stub(appContext.jsApiService, 'getReviewPostRevert')
- .returns(review);
+ sinon.stub(element.jsAPI, 'getReviewPostRevert').returns(review);
const saveStub = stubRestApi('saveChangeReview').returns(
Promise.resolve(new Response())
);
@@ -1382,7 +1379,7 @@
'This reverts commit 2000.\n\nReason ' +
'for revert: <INSERT REASONING HERE>\n';
assert.equal(confirmRevertDialog._message, msg);
- const editedMsg = msg + 'hello';
+ let editedMsg = msg + 'hello';
confirmRevertDialog._message += 'hello';
const confirmButton = queryAndAssert(
queryAndAssert(element.$.confirmRevertDialog, 'gr-dialog'),
@@ -1390,6 +1387,15 @@
);
tap(confirmButton);
await flush();
+ // Contains generic template reason so doesn't submit
+ assert.isFalse(fireActionStub.called);
+ confirmRevertDialog._message = confirmRevertDialog._message.replace(
+ '<INSERT REASONING HERE>',
+ ''
+ );
+ editedMsg = editedMsg.replace('<INSERT REASONING HERE>', '');
+ tap(confirmButton);
+ await flush();
assert.equal(fireActionStub.getCall(0).args[0], '/revert');
assert.equal(fireActionStub.getCall(0).args[1].__key, 'revert');
assert.equal(fireActionStub.getCall(0).args[3].message, editedMsg);
@@ -1548,88 +1554,6 @@
});
});
- suite('ignore change', () => {
- setup(async () => {
- sinon.stub(element, '_fireAction');
-
- const IgnoreAction = {
- __key: 'ignore',
- __type: 'change',
- __primary: false,
- method: HttpMethod.PUT,
- label: 'Ignore',
- title: 'Working...',
- enabled: true,
- };
-
- element.actions = {
- ignore: IgnoreAction,
- };
-
- element.changeNum = 2 as NumericChangeId;
- element.latestPatchNum = 2 as PatchSetNum;
-
- await element.reload();
- await flush();
- });
-
- test('make sure the ignore button is not outside of the overflow menu', () => {
- assert.isNotOk(query(element, '[data-action-key="ignore"]'));
- });
-
- test('ignoring change', async () => {
- queryAndAssert(element.$.moreActions, 'span[data-id="ignore-change"]');
- element.setActionOverflow(ActionType.CHANGE, 'ignore', false);
- await flush();
- queryAndAssert(element, '[data-action-key="ignore"]');
- assert.isNotOk(
- query(element.$.moreActions, 'span[data-id="ignore-change"]')
- );
- });
- });
-
- suite('unignore change', () => {
- setup(async () => {
- sinon.stub(element, '_fireAction');
-
- const UnignoreAction = {
- __key: 'unignore',
- __type: 'change',
- __primary: false,
- method: HttpMethod.PUT,
- label: 'Unignore',
- title: 'Working...',
- enabled: true,
- };
-
- element.actions = {
- unignore: UnignoreAction,
- };
-
- element.changeNum = 2 as NumericChangeId;
- element.latestPatchNum = 2 as PatchSetNum;
-
- await element.reload();
- await flush();
- });
-
- test('unignore button is not outside of the overflow menu', () => {
- assert.isNotOk(query(element, '[data-action-key="unignore"]'));
- });
-
- test('unignoring change', async () => {
- assert.isOk(
- query(element.$.moreActions, 'span[data-id="unignore-change"]')
- );
- element.setActionOverflow(ActionType.CHANGE, 'unignore', false);
- await flush();
- assert.isOk(query(element, '[data-action-key="unignore"]'));
- assert.isNotOk(
- query(element.$.moreActions, 'span[data-id="unignore-change"]')
- );
- });
- });
-
suite('quick approve', () => {
setup(async () => {
element.change = {
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 8b46cd8..d0126fd 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
@@ -50,7 +50,6 @@
import {customElement, property, observe} from '@polymer/decorators';
import {
AccountDetailInfo,
- AccountInfo,
BranchName,
ChangeInfo,
CommitId,
@@ -70,7 +69,7 @@
import {assertNever, unique} from '../../../utils/common-util';
import {GrEditableLabel} from '../../shared/gr-editable-label/gr-editable-label';
import {GrLinkedChip} from '../../shared/gr-linked-chip/gr-linked-chip';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {
Metadata,
isSectionSet,
@@ -88,7 +87,7 @@
} from '../../shared/gr-autocomplete/gr-autocomplete';
import {getRevertCreatedChangeIds} from '../../../utils/message-util';
import {Interaction} from '../../../constants/reporting';
-import {KnownExperimentId} from '../../../services/flags/flags';
+import {showNewSubmitRequirements} from '../../../utils/label-util';
const HASHTAG_ADD_MESSAGE = 'Add Hashtag';
@@ -190,9 +189,6 @@
@property({type: Boolean, computed: '_computeShowRequirements(change)'})
_showRequirements = false;
- @property({type: Array})
- _assignee?: AccountInfo[];
-
@property({type: Boolean, computed: '_computeIsWip(change)'})
_isWip = false;
@@ -217,21 +213,15 @@
@property({type: Object})
queryTopic?: AutocompleteQuery;
- @property({type: Boolean})
- _isSubmitRequirementsUiEnabled = false;
+ restApiService = getAppContext().restApiService;
- restApiService = appContext.restApiService;
+ private readonly reporting = getAppContext().reportingService;
- private readonly reporting = appContext.reportingService;
-
- private readonly flagsService = appContext.flagsService;
+ private readonly flagsService = getAppContext().flagsService;
override ready() {
super.ready();
this.queryTopic = (input: string) => this._getTopicSuggestions(input);
- this._isSubmitRequirementsUiEnabled = this.flagsService.isEnabled(
- KnownExperimentId.SUBMIT_REQUIREMENTS_UI
- );
}
@observe('change.labels')
@@ -240,39 +230,10 @@
}
@observe('change')
- _changeChanged(change?: ParsedChangeInfo) {
- this._assignee = change?.assignee ? [change.assignee] : [];
+ _changeChanged(_: ParsedChangeInfo) {
this._settingTopic = false;
}
- @observe('_assignee.*')
- _assigneeChanged(
- assigneeRecord: ElementPropertyDeepChange<GrChangeMetadata, '_assignee'>
- ) {
- if (!this.change || !this._isAssigneeEnabled(this.serverConfig)) {
- return;
- }
- const assignee = assigneeRecord.base;
- if (assignee?.length) {
- const acct = assignee[0];
- if (
- !acct._account_id ||
- (this.change.assignee &&
- acct._account_id === this.change.assignee._account_id)
- ) {
- return;
- }
- this.set(['change', 'assignee'], acct);
- this.restApiService.setAssignee(this.change._number, acct._account_id);
- } else {
- if (!this.change.assignee) {
- return;
- }
- this.set(['change', 'assignee'], undefined);
- this.restApiService.deleteAssignee(this.change._number);
- }
- }
-
_computeHideStrategy(change?: ParsedChangeInfo) {
return !changeIsOpen(change);
}
@@ -302,10 +263,6 @@
return change?.status === ChangeStatus.MERGED;
}
- _isAssigneeEnabled(serverConfig?: ServerInfo) {
- return !!serverConfig?.change?.enable_assignee;
- }
-
_computeStrategy(change?: ParsedChangeInfo) {
if (!change?.submit_type) {
return '';
@@ -388,10 +345,6 @@
return !mutable || !change?.actions?.hashtags?.enabled;
}
- _computeAssigneeReadOnly(mutable?: boolean, change?: ParsedChangeInfo) {
- return !mutable || !change?.actions?.assignee?.enabled;
- }
-
_computeTopicPlaceholder(_topicReadOnly?: boolean) {
// Action items in Material Design are uppercase -- placeholder label text
// is sentence case.
@@ -754,13 +707,7 @@
}
_showNewSubmitRequirements(change?: ParsedChangeInfo) {
- if (!this._isSubmitRequirementsUiEnabled) return false;
- return (change?.submit_requirements ?? []).length > 0;
- }
-
- _showNewSubmitRequirementWarning(change?: ParsedChangeInfo) {
- if (!this._isSubmitRequirementsUiEnabled) return false;
- return (change?.submit_requirements ?? []).length === 0;
+ return showNewSubmitRequirements(this.flagsService, change);
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
index 25dab25..8df5776 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
@@ -84,10 +84,13 @@
--arrow-color: var(--warning-foreground);
display: inline-block;
}
- .separatedSection {
+ .oldSeparatedSection {
margin-top: var(--spacing-l);
padding: var(--spacing-m) 0;
}
+ .separatedSection {
+ padding: var(--spacing-m) 0;
+ }
.hashtag gr-linked-chip,
.topic gr-linked-chip {
--linked-chip-text-color: var(--link-color);
@@ -113,10 +116,6 @@
--iron-icon-height: 18px;
--iron-icon-width: 18px;
}
- .submit-requirement-error {
- color: var(--deemphasized-text-color);
- padding-left: var(--metadata-horizontal-padding);
- }
</style>
<gr-external-style id="externalStyle" name="change-metadata">
<div class="metadata-header">
@@ -246,24 +245,6 @@
></gr-account-chip>
</span>
</section>
- <template is="dom-if" if="[[_isAssigneeEnabled(serverConfig)]]">
- <section
- class$="assignee [[_computeDisplayState(_showAllSections, change, _SECTION.ASSIGNEE)]]"
- >
- <span class="title">Assignee</span>
- <span class="value">
- <gr-account-list
- id="assigneeValue"
- placeholder="Set assignee..."
- max-count="1"
- accounts="{{_assignee}}"
- readonly="[[_computeAssigneeReadOnly(_mutable, change)]]"
- suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]"
- >
- </gr-account-list>
- </span>
- </section>
- </template>
<section
class$="[[_computeDisplayState(_showAllSections, change, _SECTION.REVIEWERS)]]"
>
@@ -482,27 +463,24 @@
</template>
</span>
</section>
- <div class="separatedSection">
- <template is="dom-if" if="[[_showNewSubmitRequirements(change)]]">
+ <template is="dom-if" if="[[_showNewSubmitRequirements(change)]]">
+ <div class="separatedSection">
<gr-submit-requirements
change="[[change]]"
account="[[account]]"
mutable="[[_mutable]]"
></gr-submit-requirements>
- </template>
- <template is="dom-if" if="[[!_showNewSubmitRequirements(change)]]">
+ </div>
+ </template>
+ <template is="dom-if" if="[[!_showNewSubmitRequirements(change)]]">
+ <div class="oldSeparatedSection">
<gr-change-requirements
change="{{change}}"
account="[[account]]"
mutable="[[_mutable]]"
></gr-change-requirements>
- </template>
- <template is="dom-if" if="[[_showNewSubmitRequirementWarning(change)]]">
- <div class="submit-requirement-error">
- New Submit Requirements don't work on this change.
- </div>
- </template>
- </div>
+ </div>
+ </template>
<section
id="webLinks"
hidden$="[[!_computeWebLinks(commitInfo, serverConfig)]]"
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 422c91b..338c015 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
@@ -20,7 +20,6 @@
import './gr-change-metadata';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit';
import {GrChangeMetadata} from './gr-change-metadata';
import {
createServerInfo,
@@ -35,7 +34,6 @@
createCommit,
createRevision,
createAccountDetailWithId,
- createChangeConfig,
} from '../../../test/test-data-generators';
import {
ChangeStatus,
@@ -57,8 +55,6 @@
LabelValueToDescriptionMap,
Hashtag,
} from '../../../types/common';
-import {SinonStubbedMember} from 'sinon';
-import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
import {tap} from '@polymer/iron-test-helpers/mock-interactions';
import {GrEditableLabel} from '../../shared/gr-editable-label/gr-editable-label';
import {PluginApi} from '../../../api/plugin';
@@ -70,8 +66,6 @@
const basicFixture = fixtureFromElement('gr-change-metadata');
-const pluginApi = _testOnly_initGerritPluginApi();
-
suite('gr-change-metadata tests', () => {
let element: GrChangeMetadata;
@@ -810,64 +804,6 @@
flush();
});
- suite('assignee field', () => {
- const dummyAccount = createAccountWithId();
- const change: ParsedChangeInfo = {
- ...createParsedChange(),
- actions: {
- assignee: {enabled: false},
- },
- assignee: dummyAccount,
- };
- let deleteStub: SinonStubbedMember<RestApiService['deleteAssignee']>;
- let setStub: SinonStubbedMember<RestApiService['setAssignee']>;
-
- setup(() => {
- deleteStub = stubRestApi('deleteAssignee');
- setStub = stubRestApi('setAssignee');
- element.serverConfig = {
- ...createServerInfo(),
- change: {
- ...createChangeConfig(),
- enable_assignee: true,
- },
- };
- });
-
- test('changing change recomputes _assignee', () => {
- assert.isFalse(!!element._assignee?.length);
- const change = element.change;
- change!.assignee = dummyAccount;
- element._changeChanged(change);
- assert.deepEqual(element?._assignee?.[0], dummyAccount);
- });
-
- test('modifying _assignee calls API', () => {
- assert.isFalse(!!element._assignee?.length);
- element.set('_assignee', [dummyAccount]);
- assert.isTrue(setStub.calledOnce);
- assert.deepEqual(element.change!.assignee, dummyAccount);
- element.set('_assignee', [dummyAccount]);
- assert.isTrue(setStub.calledOnce);
- element.set('_assignee', []);
- assert.isTrue(deleteStub.calledOnce);
- assert.equal(element.change!.assignee, undefined);
- element.set('_assignee', []);
- assert.isTrue(deleteStub.calledOnce);
- });
-
- test('_computeAssigneeReadOnly', () => {
- let mutable = false;
- assert.isTrue(element._computeAssigneeReadOnly(mutable, change));
- mutable = true;
- assert.isTrue(element._computeAssigneeReadOnly(mutable, change));
- change.actions!.assignee!.enabled = true;
- assert.isFalse(element._computeAssigneeReadOnly(mutable, change));
- mutable = false;
- assert.isTrue(element._computeAssigneeReadOnly(mutable, change));
- });
- });
-
test('changing topic', () => {
const newTopic = 'the new topic' as TopicName;
const setChangeTopicStub = stubRestApi('setChangeTopic').returns(
@@ -955,7 +891,7 @@
}
let hookEl: MetadataGrEndpointDecorator;
let plugin: PluginApi;
- pluginApi.install(
+ window.Gerrit.install(
p => {
plugin = p;
plugin
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
index 725bb24..821e1ce 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
@@ -33,7 +33,7 @@
LabelInfo,
} from '../../../types/common';
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {labelCompare} from '../../../utils/label-util';
import {Interaction} from '../../../constants/reporting';
@@ -85,7 +85,7 @@
@property({type: Boolean})
_showOptionalLabels = true;
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
_computeShowWip(change: ChangeInfo) {
return change.work_in_progress;
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts
index 6991c03..8161592 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts
@@ -154,7 +154,6 @@
mutable="[[mutable]]"
label="[[item.labelName]]"
label-info="[[item.labelInfo]]"
- showAlwaysOldUI
></gr-label-info>
</div>
</section>
@@ -205,7 +204,6 @@
mutable="[[mutable]]"
label="[[item.labelName]]"
label-info="[[item.labelInfo]]"
- showAlwaysOldUI
></gr-label-info>
</div>
</section>
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 ad8f72f..c90359e 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
@@ -15,34 +15,28 @@
* limitations under the License.
*/
import {LitElement, css, html} from 'lit';
-import {customElement, property} from 'lit/decorators';
+import {customElement, property, state} from 'lit/decorators';
import {subscribe} from '../../lit/subscription-controller';
import {sharedStyles} from '../../../styles/shared-styles';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {
- allRunsLatestPatchsetLatestAttempt$,
- aPluginHasRegistered$,
CheckResult,
CheckRun,
ErrorMessages,
- errorMessagesLatest$,
- loginCallbackLatest$,
- someProvidersAreLoadingFirstTime$,
- topLevelActionsLatest$,
} from '../../../services/checks/checks-model';
-import {Action, Category, Link, RunStatus} from '../../../api/checks';
+import {Action, Category, RunStatus} from '../../../api/checks';
import {fireShowPrimaryTab} from '../../../utils/event-util';
import '../../shared/gr-avatar/gr-avatar';
import '../../checks/gr-checks-action';
import {
- firstPrimaryLink,
+ compareByWorstCategory,
getResultsOf,
hasCompletedWithoutResults,
hasResults,
hasResultsOf,
iconFor,
- isRunning,
- isRunningOrHasCompleted,
+ isRunningOrScheduled,
+ isRunningScheduledOrCompleted,
isStatus,
labelFor,
} from '../../../services/checks/checks-util';
@@ -65,6 +59,8 @@
import {modifierPressed} from '../../../utils/dom-util';
import {DropdownLink} from '../../shared/gr-dropdown/gr-dropdown';
import {fontStyles} from '../../../styles/gr-font-styles';
+import {commentsModelToken} from '../../../models/comments/comments-model';
+import {resolve} from '../../../models/dependency';
export enum SummaryChipStyles {
INFO = 'info',
@@ -93,7 +89,7 @@
@property()
category?: CommentTabState;
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
static override get styles() {
return [
@@ -182,7 +178,7 @@
text = '';
@property()
- links: Link[] = [];
+ links: string[] = [];
static override get styles() {
return [
@@ -214,7 +210,7 @@
display: none;
}
.checksChip.hoverFullLength .text {
- max-width: 400px;
+ max-width: 500px;
}
:host(:hover) .checksChip.hoverFullLength {
display: inline-block;
@@ -232,6 +228,9 @@
height: var(--line-height-small);
vertical-align: top;
}
+ .checksChip a iron-icon.launch {
+ color: var(--link-color);
+ }
.checksChip.error {
color: var(--error-foreground);
border-color: var(--error-foreground);
@@ -289,18 +288,22 @@
.checksChip.check-circle-outline iron-icon {
color: var(--success-foreground);
}
- .checksChip.timelapse {
+ .checksChip.timelapse,
+ .checksChip.scheduled {
border-color: var(--gray-foreground);
background: var(--gray-background);
}
- .checksChip.timelapse:hover {
+ .checksChip.timelapse:hover,
+ .checksChip.scheduled:hover {
background: var(--gray-background-hover);
box-shadow: var(--elevation-level-1);
}
- .checksChip.timelapse:focus-within {
+ .checksChip.timelapse:focus-within,
+ .checksChip.scheduled:focus-within {
background: var(--gray-background-focus);
}
- .checksChip.timelapse iron-icon {
+ .checksChip.timelapse iron-icon,
+ .checksChip.scheduled iron-icon {
color: var(--gray-foreground);
}
`,
@@ -335,8 +338,8 @@
return html`
<div class="${clazz}" role="link" tabindex="0" aria-label="${ariaLabel}">
<iron-icon icon="${icon}"></iron-icon>
- <div class="text">${this.text}</div>
${this.renderLinks()}
+ <div class="text">${this.text}</div>
</div>
`;
}
@@ -345,7 +348,7 @@
return this.links.map(
link => html`
<a
- href="${link.url}"
+ href="${link}"
target="_blank"
@click="${this.onLinkClick}"
@keydown="${this.onLinkKeyDown}"
@@ -375,49 +378,84 @@
@customElement('gr-change-summary')
export class GrChangeSummary extends LitElement {
- @property({type: Object})
+ @state()
changeComments?: ChangeComments;
- @property({type: Array})
+ @state()
commentThreads?: CommentThread[];
- @property({type: Object})
+ @state()
selfAccount?: AccountInfo;
- @property()
+ @state()
runs: CheckRun[] = [];
- @property()
+ @state()
showChecksSummary = false;
- @property()
+ @state()
someProvidersAreLoading = false;
- @property()
+ @state()
errorMessages: ErrorMessages = {};
- @property()
+ @state()
loginCallback?: () => void;
- @property()
+ @state()
actions: Action[] = [];
private showAllChips = new Map<RunStatus | Category, boolean>();
- private checksService = appContext.checksService;
+ private getCommentsModel = resolve(this, commentsModelToken);
- constructor() {
- super();
- subscribe(this, allRunsLatestPatchsetLatestAttempt$, x => (this.runs = x));
- subscribe(this, aPluginHasRegistered$, x => (this.showChecksSummary = x));
+ private userModel = getAppContext().userModel;
+
+ private checksModel = getAppContext().checksModel;
+
+ override connectedCallback() {
+ super.connectedCallback();
subscribe(
this,
- someProvidersAreLoadingFirstTime$,
+ this.checksModel.allRunsLatestPatchsetLatestAttempt$,
+ x => (this.runs = x)
+ );
+ subscribe(
+ this,
+ this.checksModel.aPluginHasRegistered$,
+ x => (this.showChecksSummary = x)
+ );
+ subscribe(
+ this,
+ this.checksModel.someProvidersAreLoadingFirstTime$,
x => (this.someProvidersAreLoading = x)
);
- subscribe(this, errorMessagesLatest$, x => (this.errorMessages = x));
- subscribe(this, loginCallbackLatest$, x => (this.loginCallback = x));
- subscribe(this, topLevelActionsLatest$, x => (this.actions = x));
+ subscribe(
+ this,
+ this.checksModel.errorMessagesLatest$,
+ x => (this.errorMessages = x)
+ );
+ subscribe(
+ this,
+ this.checksModel.loginCallbackLatest$,
+ x => (this.loginCallback = x)
+ );
+ subscribe(
+ this,
+ this.checksModel.topLevelActionsLatest$,
+ x => (this.actions = x)
+ );
+ subscribe(
+ this,
+ this.getCommentsModel().changeComments$,
+ x => (this.changeComments = x)
+ );
+ subscribe(
+ this,
+ this.getCommentsModel().threads$,
+ x => (this.commentThreads = x)
+ );
+ subscribe(this, this.userModel.account$, x => (this.selfAccount = x));
}
static override get styles() {
@@ -548,7 +586,7 @@
}
private handleAction(e: CustomEvent<Action>) {
- this.checksService.triggerAction(e.detail);
+ this.checksModel.triggerAction(e.detail);
}
private renderOverflow(items: DropdownLink[], disabledIds: string[] = []) {
@@ -609,7 +647,7 @@
renderChecksZeroState() {
if (Object.keys(this.errorMessages).length > 0) return;
if (this.loginCallback) return;
- if (this.runs.some(isRunningOrHasCompleted)) return;
+ if (this.runs.some(isRunningScheduledOrCompleted)) return;
const msg = this.someProvidersAreLoading ? 'Loading results' : 'No results';
return html`<span role="status" class="loading zeroState">${msg}</span>`;
}
@@ -623,18 +661,19 @@
if (category === Category.SUCCESS || category === Category.INFO) {
return this.renderChecksChipsCollapsed(runs, category, count);
}
- return this.renderChecksChipsExpanded(runs, category, count);
+ return this.renderChecksChipsExpanded(runs, category);
}
renderChecksChipRunning() {
- const runs = this.runs.filter(isRunning);
- return this.renderChecksChipsExpanded(runs, RunStatus.RUNNING, () => []);
+ const runs = this.runs
+ .filter(isRunningOrScheduled)
+ .sort(compareByWorstCategory);
+ return this.renderChecksChipsExpanded(runs, RunStatus.RUNNING);
}
renderChecksChipsExpanded(
runs: CheckRun[],
- statusOrCategory: RunStatus | Category,
- resultFilter: (run: CheckRun) => CheckResult[]
+ statusOrCategory: RunStatus | Category
) {
if (runs.length === 0) return;
const showAll = this.showAllChips.get(statusOrCategory) ?? false;
@@ -644,7 +683,7 @@
return html`${runs
.slice(0, count)
.map(run =>
- this.renderChecksChipDetailed(run, statusOrCategory, resultFilter)
+ this.renderChecksChipDetailed(run, statusOrCategory)
)}${this.renderChecksChipPlusMore(statusOrCategory, more)}`;
}
@@ -687,18 +726,23 @@
private renderChecksChipDetailed(
run: CheckRun,
- statusOrCategory: RunStatus | Category,
- resultFilter: (run: CheckRun) => CheckResult[]
+ statusOrCategory: RunStatus | Category
) {
- const allPrimaryLinks = resultFilter(run)
- .map(firstPrimaryLink)
- .filter(notUndefined);
- const links = allPrimaryLinks.length === 1 ? allPrimaryLinks : [];
+ const links = [];
+ if (run.statusLink) links.push(run.statusLink);
const text = `${run.checkName}`;
const tabState: ChecksTabState = {
checkName: run.checkName,
statusOrCategory,
};
+ // Scheduled runs are rendered in the RUNNING section, but the icon of the
+ // chip must be the one for SCHEDULED.
+ if (
+ statusOrCategory === RunStatus.RUNNING &&
+ run.status === RunStatus.SCHEDULED
+ ) {
+ statusOrCategory = RunStatus.SCHEDULED;
+ }
const handler = () => this.onChipClick(tabState);
return html`<gr-checks-chip
.statusOrCategory="${statusOrCategory}"
@@ -727,7 +771,7 @@
const hasNonRunningChip = this.runs.some(
run => hasCompletedWithoutResults(run) || hasResults(run)
);
- const hasRunningChip = this.runs.some(isRunning);
+ const hasRunningChip = this.runs.some(isRunningOrScheduled);
return html`
<div>
<table>
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 b635d3f..18eda12 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
@@ -14,10 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Subscription} from 'rxjs';
import '@polymer/paper-tabs/paper-tabs';
import '../../../styles/gr-a11y-styles';
+import '../../../styles/gr-paper-styles';
import '../../../styles/shared-styles';
-import '../../diff/gr-comment-api/gr-comment-api';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
import '../../shared/gr-account-link/gr-account-link';
@@ -42,8 +43,8 @@
import '../gr-reply-dialog/gr-reply-dialog';
import '../gr-thread-list/gr-thread-list';
import '../../checks/gr-checks-tab';
+import {ChangeStarToggleStarDetail} from '../../shared/gr-change-star/gr-change-star';
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-change-view_html';
import {
KeyboardShortcutMixin,
@@ -61,19 +62,21 @@
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 {DiffViewMode} from '../../../api/diff';
import {
ChangeStatus,
DefaultBase,
PrimaryTab,
SecondaryTab,
+ DiffViewMode,
} from '../../../constants/constants';
import {NO_ROBOT_COMMENTS_THREADS_MSG} from '../../../constants/messages';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {
computeAllPatchSets,
computeLatestPatchNum,
+ findEdit,
+ findEditParentRevision,
hasEditBasedOnCurrentPatchSet,
hasEditPatchsetLoaded,
PatchSet,
@@ -107,7 +110,6 @@
CommitId,
CommitInfo,
ConfigInfo,
- EditInfo,
EditPatchSetNum,
LabelNameToInfoMap,
NumericChangeId,
@@ -127,18 +129,19 @@
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 {ChangeComments} from '../../diff/gr-comment-api/gr-comment-api';
import {
- ChangeComments,
- GrCommentApi,
-} from '../../diff/gr-comment-api/gr-comment-api';
-import {assertIsDefined, hasOwnProperty} from '../../../utils/common-util';
+ assertIsDefined,
+ hasOwnProperty,
+ query,
+} from '../../../utils/common-util';
import {GrEditControls} from '../../edit/gr-edit-controls/gr-edit-controls';
import {
CommentThread,
isDraftThread,
isRobot,
isUnresolved,
- UIDraft,
+ DraftInfo,
} from '../../../utils/comment-util';
import {
PolymerDeepPropertyChange,
@@ -174,28 +177,28 @@
fireAlert,
fireDialogChange,
fireEvent,
- firePageError,
fireReload,
fireTitleChange,
} from '../../../utils/event-util';
-import {GerritView, routerView$} from '../../../services/router/router-model';
-import {takeUntil} from 'rxjs/operators';
-import {aPluginHasRegistered$} from '../../../services/checks/checks-model';
-import {Subject} from 'rxjs';
-import {debounce, DelayedTask, throttleWrap} from '../../../utils/async-util';
+import {GerritView} from '../../../services/router/router-model';
+import {
+ debounce,
+ DelayedTask,
+ throttleWrap,
+ until,
+} from '../../../utils/async-util';
import {Interaction, Timing} from '../../../constants/reporting';
import {ChangeStates} from '../../shared/gr-change-status/gr-change-status';
import {getRevertCreatedChangeIds} from '../../../utils/message-util';
import {
- changeComments$,
- drafts$,
-} from '../../../services/comments/comments-model';
-import {
getAddedByReason,
getRemovedByReason,
hasAttention,
} from '../../../utils/attention-set-util';
import {listen} from '../../../services/shortcuts/shortcuts-service';
+import {LoadingStatus} from '../../../services/change/change-model';
+import {commentsModelToken} from '../../../models/comments/comments-model';
+import {resolve, DIPolymerElement} from '../../../models/dependency';
const MIN_LINES_FOR_COMMIT_COLLAPSE = 18;
@@ -223,7 +226,6 @@
export interface GrChangeView {
$: {
- commentAPI: GrCommentApi;
applyFixDialog: GrApplyFixDialog;
fileList: GrFileList & Element;
fileListHeader: GrFileListHeader;
@@ -233,7 +235,6 @@
downloadOverlay: GrOverlay;
downloadDialog: GrDownloadDialog;
replyOverlay: GrOverlay;
- replyDialog: GrReplyDialog;
mainContent: HTMLDivElement;
changeStar: GrChangeStar;
actions: GrChangeActions;
@@ -248,7 +249,7 @@
export type ChangeViewPatchRange = Partial<PatchRange>;
// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
+const base = KeyboardShortcutMixin(DIPolymerElement);
@customElement('gr-change-view')
export class GrChangeView extends base {
@@ -274,12 +275,6 @@
* @event show-auth-required
*/
- private readonly reporting = appContext.reportingService;
-
- private readonly jsAPI = appContext.jsApiService;
-
- private readonly changeService = appContext.changeService;
-
/**
* URL params passed from the router.
*/
@@ -337,7 +332,7 @@
_canStartReview?: boolean;
@property({type: Object, observer: '_changeChanged'})
- _change?: ChangeInfo | ParsedChangeInfo;
+ _change?: ParsedChangeInfo;
@property({type: Object, computed: '_getRevisionInfo(_change)'})
_revisionInfo?: RevisionInfoClass;
@@ -358,7 +353,7 @@
_changeNum?: NumericChangeId;
@property({type: Object})
- _diffDrafts?: {[path: string]: UIDraft[]} = {};
+ _diffDrafts?: {[path: string]: DraftInfo[]} = {};
@property({type: Boolean})
_editingCommitMessage = false;
@@ -386,9 +381,6 @@
@property({type: Object})
_messages = NO_ROBOT_COMMENTS_THREADS_MSG;
- @property({type: Number})
- _lineHeight?: number;
-
@property({type: Object})
_patchRange?: ChangeViewPatchRange;
@@ -401,6 +393,9 @@
@property({type: Object})
_selectedRevision?: RevisionInfo | EditRevisionInfo;
+ /**
+ * <gr-change-actions> populates this via two-way data binding.
+ */
@property({type: Object})
_currentRevisionActions?: ActionNameToActionInfoMap;
@@ -518,7 +513,7 @@
_activeTabs: string[] = [PrimaryTab.FILES, SecondaryTab.CHANGE_LOG];
@property({type: Boolean})
- unresolvedOnly = false;
+ unresolvedOnly = true;
@property({type: Boolean})
_showAllRobotComments = false;
@@ -543,20 +538,16 @@
@property({type: String})
scrollCommentId?: UrlEncodedCommentId;
+ /** Just reflects the `opened` prop of the overlay. */
+ @property({type: Boolean})
+ replyOverlayOpened = false;
+
@property({
type: Array,
computed: '_computeResolveWeblinks(_change, _commitInfo, _serverConfig)',
})
resolveWeblinks?: GeneratedWebLink[];
- restApiService = appContext.restApiService;
-
- private readonly commentsService = appContext.commentsService;
-
- private readonly shortcuts = appContext.shortcutsService;
-
- private replyDialogResizeObserver?: ResizeObserver;
-
override keyboardShortcuts(): ShortcutListener[] {
return [
listen(Shortcut.SEND_REPLY, _ => {}), // docOnly
@@ -603,7 +594,29 @@
];
}
- disconnected$ = new Subject();
+ // Accessed in tests.
+ readonly reporting = getAppContext().reportingService;
+
+ readonly jsAPI = getAppContext().jsApiService;
+
+ private readonly checksModel = getAppContext().checksModel;
+
+ readonly restApiService = getAppContext().restApiService;
+
+ // Private but used in tests.
+ readonly userModel = getAppContext().userModel;
+
+ // Private but used in tests.
+ readonly changeModel = getAppContext().changeModel;
+
+ private readonly routerModel = getAppContext().routerModel;
+
+ // Private but used in tests.
+ readonly getCommentsModel = resolve(this, commentsModelToken);
+
+ private readonly shortcuts = getAppContext().shortcutsService;
+
+ private subscriptions: Subscription[] = [];
private replyRefitTask?: DelayedTask;
@@ -611,23 +624,17 @@
private lastStarredTimestamp?: number;
- override ready() {
- super.ready();
- aPluginHasRegistered$.pipe(takeUntil(this.disconnected$)).subscribe(b => {
- this._showChecksTab = b;
- });
- routerView$.pipe(takeUntil(this.disconnected$)).subscribe(view => {
- this.isViewCurrent = view === GerritView.CHANGE;
- });
- drafts$.pipe(takeUntil(this.disconnected$)).subscribe(drafts => {
- this._diffDrafts = {...drafts};
- });
- changeComments$
- .pipe(takeUntil(this.disconnected$))
- .subscribe(changeComments => {
- this._changeComments = changeComments;
- });
- }
+ private diffViewMode?: DiffViewMode;
+
+ /**
+ * If the user comes back to the change page we want to remember the scroll
+ * position when we re-render the page as is.
+ */
+ private scrollPosition?: number;
+
+ /** Simply reflects the router-model value. */
+ // visible for testing
+ routerPatchNum?: PatchSetNum;
constructor() {
super();
@@ -649,8 +656,49 @@
this.addEventListener('open-reply-dialog', () => this._openReplyDialog());
}
+ private setupSubscriptions() {
+ this.subscriptions.push(
+ this.checksModel.aPluginHasRegistered$.subscribe(b => {
+ this._showChecksTab = b;
+ })
+ );
+ this.subscriptions.push(
+ this.routerModel.routerView$.subscribe(view => {
+ this.isViewCurrent = view === GerritView.CHANGE;
+ })
+ );
+ this.subscriptions.push(
+ this.routerModel.routerPatchNum$.subscribe(patchNum => {
+ this.routerPatchNum = patchNum;
+ })
+ );
+ this.subscriptions.push(
+ this.getCommentsModel().drafts$.subscribe(drafts => {
+ this._diffDrafts = {...drafts};
+ })
+ );
+ this.subscriptions.push(
+ this.userModel.preferenceDiffViewMode$.subscribe(diffViewMode => {
+ this.diffViewMode = diffViewMode;
+ })
+ );
+ this.subscriptions.push(
+ this.getCommentsModel().changeComments$.subscribe(changeComments => {
+ this._changeComments = changeComments;
+ })
+ );
+ this.subscriptions.push(
+ this.changeModel.change$.subscribe(change => {
+ // The change view is tied to a specific change number, so don't update
+ // _change to undefined.
+ if (change) this._change = change;
+ })
+ );
+ }
+
override connectedCallback() {
super.connectedCallback();
+ this.setupSubscriptions();
this._throttledToggleChangeStar = throttleWrap<KeyboardEvent>(_ =>
this._handleToggleChangeStar()
);
@@ -666,14 +714,8 @@
this._account = acct;
});
}
- this._setDiffViewMode();
});
- this.replyDialogResizeObserver = new ResizeObserver(() =>
- this.$.replyOverlay.center()
- );
- this.replyDialogResizeObserver.observe(this.$.replyDialog);
-
getPluginLoader()
.awaitPluginsLoaded()
.then(() => {
@@ -700,6 +742,7 @@
this.addEventListener('open-fix-preview', e => this._onOpenFixPreview(e));
this.addEventListener('close-fix-preview', e => this._onCloseFixPreview(e));
document.addEventListener('visibilitychange', this.handleVisibilityChange);
+ document.addEventListener('scroll', this.handleScroll);
this.addEventListener(EventType.SHOW_PRIMARY_TAB, e =>
this._setActivePrimaryTab(e)
@@ -713,11 +756,15 @@
}
override disconnectedCallback() {
- this.disconnected$.next();
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions = [];
document.removeEventListener(
'visibilitychange',
this.handleVisibilityChange
);
+ document.removeEventListener('scroll', this.handleScroll);
this.replyRefitTask?.cancel();
this.scrollTask?.cancel();
@@ -735,23 +782,14 @@
return this.shadowRoot!.querySelector<GrThreadList>('gr-thread-list');
}
- _setDiffViewMode(opt_reset?: boolean) {
- if (!opt_reset && this.viewState.diffViewMode) {
- return;
- }
-
- return this._getPreferences()
- .then(prefs => {
- if (!this.viewState.diffMode && prefs) {
- this.set('viewState.diffMode', prefs.default_diff_view);
- }
- })
- .then(() => {
- if (!this.viewState.diffMode) {
- this.set('viewState.diffMode', 'SIDE_BY_SIDE');
- }
- });
- }
+ private readonly handleScroll = () => {
+ if (!this.isViewCurrent) return;
+ this.scrollTask = debounce(
+ this.scrollTask,
+ () => (this.scrollPosition = document.documentElement.scrollTop),
+ 150
+ );
+ };
_onOpenFixPreview(e: OpenFixPreviewEvent) {
this.$.applyFixDialog.open(e);
@@ -762,10 +800,12 @@
}
_handleToggleDiffMode() {
- if (this.viewState.diffMode === DiffViewMode.SIDE_BY_SIDE) {
- this.$.fileListHeader.setDiffViewMode(DiffViewMode.UNIFIED);
+ if (this.diffViewMode === DiffViewMode.SIDE_BY_SIDE) {
+ this.userModel.updatePreferences({diff_view: DiffViewMode.UNIFIED});
} else {
- this.$.fileListHeader.setDiffViewMode(DiffViewMode.SIDE_BY_SIDE);
+ this.userModel.updatePreferences({
+ diff_view: DiffViewMode.SIDE_BY_SIDE,
+ });
}
}
@@ -857,6 +897,13 @@
this._tabState = e.detail.tabState;
}
+ /**
+ * Currently there is a bug in this code where this.unresolvedOnly is only
+ * assigned the correct value when _onPaperTabClick is triggered which is
+ * only triggered when user explicitly clicks on the tab however the comments
+ * tab can also be opened via the url in which case the correct value to
+ * unresolvedOnly is never assigned.
+ */
_onPaperTabClick(e: MouseEvent) {
let target = e.target as HTMLElement | null;
let tabName: string | undefined;
@@ -868,11 +915,12 @@
} while (target);
if (tabName === PrimaryTab.COMMENT_THREADS) {
- // Show unresolved threads by default only if they are present
+ // Show unresolved threads by default
+ // Show resolved threads only if no unresolved threads exist
const hasUnresolvedThreads =
(this._commentThreads ?? []).filter(thread => isUnresolved(thread))
.length > 0;
- if (hasUnresolvedThreads) this.unresolvedOnly = true;
+ if (!hasUnresolvedThreads) this.unresolvedOnly = false;
}
this.reporting.reportInteraction(Interaction.SHOW_TAB, {
@@ -1003,8 +1051,8 @@
.sort((a, b) => (b.value as number) - (a.value as number));
}
- _handleCurrentRevisionUpdate(currentRevision: RevisionInfo) {
- this._currentRobotCommentsPatchSet = currentRevision._number;
+ _handleCurrentRevisionUpdate(currentRevision?: RevisionInfo) {
+ this._currentRobotCommentsPatchSet = currentRevision?._number;
}
_handleRobotCommentPatchSetChanged(e: CustomEvent<{value: string}>) {
@@ -1062,7 +1110,7 @@
_handleReplyTap(e: MouseEvent) {
e.preventDefault();
- this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
+ this._openReplyDialog(FocusTarget.ANY);
}
onReplyOverlayCanceled() {
@@ -1106,8 +1154,7 @@
.split('\n')
.map(line => '> ' + line)
.join('\n') + '\n\n';
- this.$.replyDialog.quote = quoteStr;
- this._openReplyDialog(this.$.replyDialog.FocusTarget.BODY);
+ this._openReplyDialog(FocusTarget.BODY, quoteStr);
}
_handleHideBackgroundContent() {
@@ -1144,9 +1191,9 @@
}
_handleShowReplyDialog(e: CustomEvent<{value: {ccsOnly: boolean}}>) {
- let target = this.$.replyDialog.FocusTarget.REVIEWERS;
+ let target = FocusTarget.REVIEWERS;
if (e.detail.value && e.detail.value.ccsOnly) {
- target = this.$.replyDialog.FocusTarget.CCS;
+ target = FocusTarget.CCS;
}
this._openReplyDialog(target);
}
@@ -1185,6 +1232,24 @@
return this._changeNum !== this.params?.changeNum;
}
+ hasPatchRangeChanged(value: AppElementChangeViewParams) {
+ if (!this._patchRange) return false;
+ if (this._patchRange.basePatchNum !== value.basePatchNum) return true;
+ return this.hasPatchNumChanged(value);
+ }
+
+ hasPatchNumChanged(value: AppElementChangeViewParams) {
+ if (!this._patchRange) return false;
+ if (value.patchNum !== undefined) {
+ return this._patchRange.patchNum !== value.patchNum;
+ } else {
+ // value.patchNum === undefined specifies the latest patchset
+ return (
+ this._patchRange.patchNum !== computeLatestPatchNum(this._allPatchSets)
+ );
+ }
+ }
+
_paramsChanged(value: AppElementChangeViewParams) {
if (value.view !== GerritView.CHANGE) {
this._initialLoadComplete = false;
@@ -1208,40 +1273,67 @@
if (value.basePatchNum === undefined)
value.basePatchNum = ParentPatchSetNum;
- const patchChanged =
- this._patchRange &&
- value.patchNum !== undefined &&
- (this._patchRange.patchNum !== value.patchNum ||
- this._patchRange.basePatchNum !== value.basePatchNum);
+ const patchChanged = this.hasPatchRangeChanged(value);
+ let patchNumChanged = this.hasPatchNumChanged(value);
- let rightPatchNumChanged =
- this._patchRange &&
- value.patchNum !== undefined &&
- this._patchRange.patchNum !== value.patchNum;
-
- const patchRange: ChangeViewPatchRange = {
+ this._patchRange = {
patchNum: value.patchNum,
basePatchNum: value.basePatchNum,
};
-
- this.$.fileList.collapseAllDiffs();
- this._patchRange = patchRange;
this.scrollCommentId = value.commentId;
const patchKnown =
- !patchRange.patchNum ||
- (this._allPatchSets ?? []).some(ps => ps.num === patchRange.patchNum);
+ !this._patchRange.patchNum ||
+ (this._allPatchSets ?? []).some(
+ ps => ps.num === this._patchRange!.patchNum
+ );
+ // _allPatchsets does not know value.patchNum so force a reload.
+ const forceReload = value.forceReload || !patchKnown;
- // If the change has already been loaded and the parameter change is only
- // in the patch range, then don't do a full reload.
- if (this._changeNum !== undefined && patchChanged && patchKnown) {
- if (!patchRange.patchNum) {
- patchRange.patchNum = computeLatestPatchNum(this._allPatchSets);
- rightPatchNumChanged = true;
+ // If changeNum is defined that means the change has already been
+ // rendered once before so a full reload is not required.
+ if (this._changeNum !== undefined && !forceReload) {
+ if (!this._patchRange.patchNum) {
+ this._patchRange = {
+ ...this._patchRange,
+ patchNum: computeLatestPatchNum(this._allPatchSets),
+ };
+ patchNumChanged = true;
}
- this._reloadPatchNumDependentResources(rightPatchNumChanged).then(() => {
- this._sendShowChangeEvent();
- });
+ if (patchChanged) {
+ // We need to collapse all diffs when params change so that a non
+ // existing diff is not requested. See Issue 125270 for more details.
+ this.$.fileList.collapseAllDiffs();
+ this._reloadPatchNumDependentResources(patchNumChanged).then(() => {
+ this._sendShowChangeEvent();
+ });
+ }
+
+ // If there is no change in patchset or changeNum, such as when user goes
+ // to the diff view and then comes back to change page then there is no
+ // need to reload anything and we render the change view component as is.
+ document.documentElement.scrollTop = this.scrollPosition ?? 0;
+ this.reporting.reportInteraction('change-view-re-rendered');
+ this.updateTitle(this._change);
+ // We still need to check if post load tasks need to be done such as when
+ // user wants to open the reply dialog when in the diff page, the change
+ // page should open the reply dialog
+ this._performPostLoadTasks();
+ return;
+ }
+
+ // We need to collapse all diffs when params change so that a non existing
+ // diff is not requested. See Issue 125270 for more details.
+ this.$.fileList.collapseAllDiffs();
+
+ // If the change was loaded before, then we are firing a 'reload' event
+ // instead of calling `loadData()` directly for two reasons:
+ // 1. We want to avoid code such as `this._initialLoadComplete = false` that
+ // is only relevant for the initial load of a change.
+ // 2. We have to somehow trigger the change-model reloading. Otherwise
+ // this._change is not updated.
+ if (this._changeNum) {
+ fireReload(this);
return;
}
@@ -1260,8 +1352,8 @@
_initActiveTabs(params?: AppElementChangeViewParams) {
let primaryTab = PrimaryTab.FILES;
- if (params && params.queryMap && params.queryMap.has('tab')) {
- primaryTab = params.queryMap.get('tab') as PrimaryTab;
+ if (params?.tab) {
+ primaryTab = params?.tab as PrimaryTab;
} else if (params && 'commentId' in params) {
primaryTab = PrimaryTab.COMMENT_THREADS;
}
@@ -1287,7 +1379,6 @@
_performPostLoadTasks() {
this._maybeShowReplyDialog();
this._maybeShowRevertDialog();
- this._maybeShowDownloadDialog();
this._sendShowChangeEvent();
@@ -1337,13 +1428,12 @@
if (!this._patchRange)
throw new Error('missing required _patchRange property');
const hash = PREFIX + e.detail.id;
- const url = GerritNav.getUrlForChange(
- this._change,
- this._patchRange.patchNum,
- this._patchRange.basePatchNum,
- this._editMode,
- hash
- );
+ const url = GerritNav.getUrlForChange(this._change, {
+ patchNum: this._patchRange.patchNum,
+ basePatchNum: this._patchRange.basePatchNum,
+ isEdit: this._editMode,
+ messageHash: hash,
+ });
history.replaceState(null, '', url);
}
@@ -1397,34 +1487,30 @@
}
if (this.viewState.showReplyDialog) {
- this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
+ this._openReplyDialog(FocusTarget.ANY);
this.set('viewState.showReplyDialog', false);
}
});
}
- _maybeShowDownloadDialog() {
- if (this.viewState.showDownloadDialog) {
- this._handleOpenDownloadDialog();
- this.set('viewState.showDownloadDialog', false);
- }
- }
-
_resetFileListViewState() {
this.set('viewState.selectedFileIndex', 0);
if (
!!this.viewState.changeNum &&
this.viewState.changeNum !== this._changeNum
) {
- // Reset the diff mode to null when navigating from one change to
- // another, so that the user's preference is restored.
- this._setDiffViewMode(true);
this.set('_numFilesShown', DEFAULT_NUM_FILES_SHOWN);
}
this.set('viewState.changeNum', this._changeNum);
this.set('viewState.patchRange', this._patchRange);
}
+ private updateTitle(change?: ChangeInfo | ParsedChangeInfo) {
+ if (!change) return;
+ const title = change.subject + ' (' + change.change_id.substr(0, 9) + ')';
+ fireTitleChange(this, title);
+ }
+
_changeChanged(change?: ChangeInfo | ParsedChangeInfo) {
if (!change || !this._patchRange || !this._allPatchSets) {
return;
@@ -1440,9 +1526,7 @@
);
this.set('_patchRange.basePatchNum', parent);
-
- const title = change.subject + ' (' + change.change_id.substr(0, 9) + ')';
- fireTitleChange(this, title);
+ this.updateTitle(change);
}
/**
@@ -1476,12 +1560,16 @@
return 'PARENT';
}
- _computeChangeUrl(change: ChangeInfo) {
- return GerritNav.getUrlForChange(change);
+ // Polymer was converting true to "true"(type string) automatically hence
+ // forceReload is of type string instead of boolean.
+ _computeChangeUrl(change: ChangeInfo, forceReload?: string) {
+ return GerritNav.getUrlForChange(change, {
+ forceReload: !!forceReload,
+ });
}
_computeReplyButtonLabel(
- drafts?: {[path: string]: UIDraft[]},
+ drafts?: {[path: string]: DraftInfo[]},
canStartReview?: boolean
) {
if (drafts === undefined || canStartReview === undefined) {
@@ -1506,7 +1594,7 @@
fireEvent(this, 'show-auth-required');
return;
}
- this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
+ this._openReplyDialog(FocusTarget.ANY);
});
}
@@ -1562,7 +1650,9 @@
fireAlert(this, 'Base is already selected.');
return;
}
- GerritNav.navigateToChange(this._change, this._patchRange.patchNum);
+ GerritNav.navigateToChange(this._change, {
+ patchNum: this._patchRange.patchNum,
+ });
}
_handleDiffBaseAgainstLeft() {
@@ -1573,7 +1663,9 @@
fireAlert(this, 'Left is already base.');
return;
}
- GerritNav.navigateToChange(this._change, this._patchRange.basePatchNum);
+ GerritNav.navigateToChange(this._change, {
+ patchNum: this._patchRange.basePatchNum,
+ });
}
_handleDiffAgainstLatest() {
@@ -1585,11 +1677,10 @@
fireAlert(this, 'Latest is already selected.');
return;
}
- GerritNav.navigateToChange(
- this._change,
- latestPatchNum,
- this._patchRange.basePatchNum
- );
+ GerritNav.navigateToChange(this._change, {
+ patchNum: latestPatchNum,
+ basePatchNum: this._patchRange.basePatchNum,
+ });
}
_handleDiffRightAgainstLatest() {
@@ -1601,11 +1692,10 @@
fireAlert(this, 'Right is already latest.');
return;
}
- GerritNav.navigateToChange(
- this._change,
- latestPatchNum,
- this._patchRange.patchNum as BasePatchSetNum
- );
+ GerritNav.navigateToChange(this._change, {
+ patchNum: latestPatchNum,
+ basePatchNum: this._patchRange.patchNum as BasePatchSetNum,
+ });
}
_handleDiffBaseAgainstLatest() {
@@ -1620,7 +1710,7 @@
fireAlert(this, 'Already diffing base against latest.');
return;
}
- GerritNav.navigateToChange(this._change, latestPatchNum);
+ GerritNav.navigateToChange(this._change, {patchNum: latestPatchNum});
}
_handleToggleChangeStar() {
@@ -1691,21 +1781,22 @@
});
}
- _openReplyDialog(section?: FocusTarget) {
+ _openReplyDialog(focusTarget?: FocusTarget, quote?: string) {
if (!this._change) return;
- this.$.replyOverlay.open().finally(() => {
+ const overlay = this.$.replyOverlay;
+ overlay.open().finally(async () => {
// the following code should be executed no matter open succeed or not
+ const dialog = query<GrReplyDialog>(this, '#replyDialog');
+ assertIsDefined(dialog, 'reply dialog');
this._resetReplyOverlayFocusStops();
- this.$.replyDialog.open(section);
+ dialog.open(focusTarget, quote);
+ const observer = new ResizeObserver(() => overlay.center());
+ observer.observe(dialog);
});
fireDialogChange(this, {opened: true});
this._changeViewAriaHidden = true;
}
- _handleGetChangeDetailError(response?: Response | null) {
- firePageError(response);
- }
-
_getLoggedIn() {
return this.restApiService.getLoggedIn();
}
@@ -1738,9 +1829,12 @@
* Utility function to make the necessary modifications to a change in the
* case an edit exists.
*/
- _processEdit(change: ParsedChangeInfo, edit?: EditInfo | false) {
+ _processEdit(change: ParsedChangeInfo) {
+ const revisions = Object.values(change.revisions || {});
+ const editRev = findEdit(revisions);
+ const editParentRev = findEditParentRevision(revisions);
if (
- !edit &&
+ !editRev &&
this._patchRange?.patchNum === EditPatchSetNum &&
changeIsOpen(change)
) {
@@ -1750,49 +1844,34 @@
}
if (
- !edit &&
+ !editRev &&
(changeIsMerged(change) || changeIsAbandoned(change)) &&
this._editMode
) {
fireAlert(
this,
- 'Change edits cannot be created if change is merged or abandoned. Redirected to non edit mode.'
+ 'Change edits cannot be created if change is merged or abandoned. Redirecting to non edit mode.'
);
fireReload(this, true);
return;
}
- if (!edit) return;
+ if (!editRev) return;
+ assertIsDefined(this._patchRange, '_patchRange');
+ assertIsDefined(editRev.commit.commit, 'editRev.commit.commit');
+ assertIsDefined(editParentRev, 'editParentRev');
- if (!this._patchRange)
- throw new Error('missing required _patchRange property');
-
- if (!edit.commit.commit) throw new Error('undefined edit.commit.commit');
- const changeWithEdit = change;
- if (changeWithEdit.revisions)
- changeWithEdit.revisions[edit.commit.commit] = {
- _number: EditPatchSetNum,
- basePatchNum: edit.base_patch_set_number,
- commit: edit.commit,
- fetch: edit.fetch,
- };
-
- // If the edit is based on the most recent patchset, load it by
- // default, unless another patch set to load was specified in the URL.
- if (
- !this._patchRange.patchNum &&
- changeWithEdit.current_revision === edit.base_revision
- ) {
- changeWithEdit.current_revision = edit.commit.commit;
+ const latestPsNum = computeLatestPatchNum(computeAllPatchSets(change));
+ // If the change was loaded without a specific patchset, then this normally
+ // means that the *latest* patchset should be loaded. But if there is an
+ // active edit, then automatically switch to that edit as the current
+ // patchset.
+ // TODO: This goes together with `change.current_revision` being set, which
+ // is under change-model control. `_patchRange.patchNum` should eventually
+ // also be model managed, so we can reconcile these two code snippets into
+ // one location.
+ if (!this.routerPatchNum && latestPsNum === editParentRev._number) {
this.set('_patchRange.patchNum', EditPatchSetNum);
- // Because edits are fibbed as revisions and added to the revisions
- // array, and revision actions are always derived from the 'latest'
- // patch set, we must copy over actions from the patch set base.
- // Context: Issue 7243
- if (changeWithEdit.revisions) {
- changeWithEdit.revisions[edit.commit.commit].actions =
- changeWithEdit.revisions[edit.base_revision].actions;
- }
}
}
@@ -1822,81 +1901,73 @@
});
}
- _getChangeDetail() {
- if (!this._changeNum)
- throw new Error('missing required changeNum property');
- const detailCompletes = this.restApiService.getChangeDetail(
- this._changeNum,
- r => this._handleGetChangeDetailError(r)
- );
- const editCompletes = this._getEdit();
+ /**
+ * Process edits
+ * Check if a revert of this change has been submitted
+ * Calculate selected revision
+ */
+ // private but used in tests
+ async performPostChangeLoadTasks() {
+ assertIsDefined(this._changeNum, '_changeNum');
+
const prefCompletes = this._getPreferences();
-
- return Promise.all([detailCompletes, editCompletes, prefCompletes]).then(
- ([change, edit, prefs]) => {
- this._prefs = prefs;
-
- if (!change) {
- return false;
- }
- this._processEdit(change, edit);
- // Issue 4190: Coalesce missing topics to null.
- // TODO(TS): code needs second thought,
- // it might be that nulls were assigned to trigger some bindings
- if (!change.topic) {
- change.topic = null as unknown as undefined;
- }
- if (!change.reviewer_updates) {
- change.reviewer_updates = null as unknown as undefined;
- }
- const latestRevisionSha = this._getLatestRevisionSHA(change);
- if (!latestRevisionSha)
- throw new Error('Could not find latest Revision Sha');
- const currentRevision = change.revisions[latestRevisionSha];
- if (currentRevision.commit && currentRevision.commit.message) {
- this._latestCommitMessage = this._prepareCommitMsgForLinkify(
- currentRevision.commit.message
- );
- } else {
- this._latestCommitMessage = null;
- }
-
- const lineHeight = getComputedStyle(this).lineHeight;
-
- // Slice returns a number as a string, convert to an int.
- this._lineHeight = Number(lineHeight.slice(0, lineHeight.length - 2));
-
- this.changeService.updateChange(change);
- this._change = change;
- this.computeRevertSubmitted(change);
- if (
- !this._patchRange ||
- !this._patchRange.patchNum ||
- this._patchRange.patchNum === currentRevision._number
- ) {
- // CommitInfo.commit is optional, and may need patching.
- if (currentRevision.commit && !currentRevision.commit.commit) {
- currentRevision.commit.commit = latestRevisionSha as CommitId;
- }
- this._commitInfo = currentRevision.commit;
- this._selectedRevision = currentRevision;
- // TODO: Fetch and process files.
- } else {
- if (!this._change?.revisions || !this._patchRange) return false;
- this._selectedRevision = Object.values(this._change.revisions).find(
- revision => {
- // edit patchset is a special one
- const thePatchNum = this._patchRange!.patchNum;
- if (thePatchNum === 'edit') {
- return revision._number === thePatchNum;
- }
- return revision._number === Number(`${thePatchNum}`);
- }
- );
- }
- return true;
- }
+ await until(
+ this.changeModel.changeLoadingStatus$,
+ status => status === LoadingStatus.LOADED
);
+ this._prefs = await prefCompletes;
+
+ if (!this._change) return false;
+
+ this._processEdit(this._change);
+ // Issue 4190: Coalesce missing topics to null.
+ // TODO(TS): code needs second thought,
+ // it might be that nulls were assigned to trigger some bindings
+ if (!this._change.topic) {
+ this._change.topic = null as unknown as undefined;
+ }
+ if (!this._change.reviewer_updates) {
+ this._change.reviewer_updates = null as unknown as undefined;
+ }
+ const latestRevisionSha = this._getLatestRevisionSHA(this._change);
+ if (!latestRevisionSha)
+ throw new Error('Could not find latest Revision Sha');
+ const currentRevision = this._change.revisions[latestRevisionSha];
+ if (currentRevision.commit && currentRevision.commit.message) {
+ this._latestCommitMessage = this._prepareCommitMsgForLinkify(
+ currentRevision.commit.message
+ );
+ } else {
+ this._latestCommitMessage = null;
+ }
+
+ this.computeRevertSubmitted(this._change);
+ if (
+ !this._patchRange ||
+ !this._patchRange.patchNum ||
+ this._patchRange.patchNum === currentRevision._number
+ ) {
+ // CommitInfo.commit is optional, and may need patching.
+ if (currentRevision.commit && !currentRevision.commit.commit) {
+ currentRevision.commit.commit = latestRevisionSha as CommitId;
+ }
+ this._commitInfo = currentRevision.commit;
+ this._selectedRevision = currentRevision;
+ // TODO: Fetch and process files.
+ } else {
+ if (!this._change?.revisions || !this._patchRange) return false;
+ this._selectedRevision = Object.values(this._change.revisions).find(
+ revision => {
+ // edit patchset is a special one
+ const thePatchNum = this._patchRange!.patchNum;
+ if (thePatchNum === 'edit') {
+ return revision._number === thePatchNum;
+ }
+ return revision._number === Number(`${thePatchNum}`);
+ }
+ );
+ }
+ return true;
}
_isSubmitEnabled(revisionActions: ActionNameToActionInfoMap) {
@@ -1915,12 +1986,6 @@
}
}
- _getEdit() {
- if (!this._changeNum)
- return Promise.reject(new Error('missing required changeNum property'));
- return this.restApiService.getChangeEdit(this._changeNum, true);
- }
-
_getLatestCommitMessage() {
if (!this._changeNum)
throw new Error('missing required changeNum property');
@@ -1952,56 +2017,17 @@
return latestRev;
}
- _getCommitInfo() {
- if (!this._changeNum)
- throw new Error('missing required _changeNum property');
- if (!this._patchRange)
- throw new Error('missing required _patchRange property');
- if (this._patchRange.patchNum === undefined)
- throw new Error('missing required patchNum property');
-
- // We only call _getEdit if the patchset number is an edit.
- // We have to do this to ensure we can tell if an edit
- // exists or not.
- // This safely works even if a edit does not exist.
- if (this._patchRange!.patchNum! === EditPatchSetNum) {
- return this._getEdit().then(edit => {
- if (!edit) {
- return Promise.resolve();
- }
-
- return this._getChangeCommitInfo();
- });
- }
-
- return this._getChangeCommitInfo();
- }
-
- _getChangeCommitInfo() {
+ // visible for testing
+ loadAndSetCommitInfo() {
+ assertIsDefined(this._changeNum, '_changeNum');
+ assertIsDefined(this._patchRange?.patchNum, '_patchRange.patchNum');
return this.restApiService
- .getChangeCommitInfo(this._changeNum!, this._patchRange!.patchNum!)
+ .getChangeCommitInfo(this._changeNum, this._patchRange!.patchNum)
.then(commitInfo => {
this._commitInfo = commitInfo;
});
}
- /**
- * Fetches a new changeComment object, and data for all types of comments
- * (comments, robot comments, draft comments) is requested.
- */
- _reloadComments() {
- // We are resetting all comment related properties, because we want to avoid
- // a new change being loaded and then paired with outdated comments.
- this._changeComments = undefined;
- this._commentThreads = undefined;
- this._draftCommentThreads = undefined;
- this._robotCommentThreads = undefined;
- if (!this._changeNum)
- throw new Error('missing required changeNum property');
-
- this.commentsService.loadAll(this._changeNum, this._patchRange?.patchNum);
- }
-
@observe('_changeComments')
changeCommentsChanged(comments?: ChangeComments) {
if (!comments) return;
@@ -2027,8 +2053,8 @@
*
* @param isLocationChange Reloads the related changes
* when true and ends reporting events that started on location change.
- * @param clearPatchset Reloads the related changes
- * ignoring any patchset choice made.
+ * @param clearPatchset Reloads the change ignoring any patchset
+ * choice made.
* @return A promise that resolves when the core data has loaded.
* Some non-core data loading may still be in-flight when the core data
* promise resolves.
@@ -2036,7 +2062,9 @@
loadData(isLocationChange?: boolean, clearPatchset?: boolean): Promise<void> {
if (this.isChangeObsolete()) return Promise.resolve();
if (clearPatchset && this._change) {
- GerritNav.navigateToChange(this._change);
+ GerritNav.navigateToChange(this._change, {
+ forceReload: true,
+ });
return Promise.resolve();
}
this._loading = true;
@@ -2048,7 +2076,11 @@
// Resolves when the change detail and the edit patch set (if available)
// are loaded.
- const detailCompletes = this._getChangeDetail();
+ const detailCompletes = until(
+ this.changeModel.changeLoadingStatus$,
+ status => status === LoadingStatus.LOADED
+ );
+ this.performPostChangeLoadTasks();
allDataPromises.push(detailCompletes);
// Resolves when the loading flag is set to false, meaning that some
@@ -2070,14 +2102,12 @@
});
// Resolves when the project config has successfully loaded.
- const projectConfigLoaded = detailCompletes.then(success => {
- if (!success) return Promise.resolve();
+ const projectConfigLoaded = detailCompletes.then(() => {
+ if (!this._change) return Promise.resolve();
return this._getProjectConfig();
});
allDataPromises.push(projectConfigLoaded);
- this._reloadComments();
-
let coreDataPromise;
// If the patch number is specified
@@ -2094,8 +2124,6 @@
loadingFlagSet,
]);
- // _getChangeDetail triggers reload of change actions already.
-
// The core data is loaded when mergeability is known.
coreDataPromise = detailAndPatchResourcesLoaded.then(() =>
this._getMergeability()
@@ -2178,17 +2206,24 @@
* Kicks off requests for resources that rely on the patch range
* (`this._patchRange`) being defined.
*/
- _reloadPatchNumDependentResources(rightPatchNumChanged?: boolean) {
+ _reloadPatchNumDependentResources(patchNumChanged?: boolean) {
assertIsDefined(this._changeNum, '_changeNum');
if (!this._patchRange?.patchNum) throw new Error('missing patchNum');
- const promises = [this._getCommitInfo(), this.$.fileList.reload()];
- if (rightPatchNumChanged)
+ const promises = [this.loadAndSetCommitInfo(), this.$.fileList.reload()];
+ if (patchNumChanged) {
promises.push(
- this.$.commentAPI.reloadPortedComments(
+ this.getCommentsModel().reloadPortedComments(
this._changeNum,
this._patchRange?.patchNum
)
);
+ promises.push(
+ this.getCommentsModel().reloadPortedDrafts(
+ this._changeNum,
+ this._patchRange?.patchNum
+ )
+ );
+ }
return Promise.all(promises);
}
@@ -2288,13 +2323,12 @@
}
this._updateCheckTimerHandle = window.setTimeout(() => {
- if (!this.isViewCurrent) {
+ if (!this.isViewCurrent || !this._change) {
this._startUpdateCheckTimer();
return;
}
- assertIsDefined(this._change, '_change');
const change = this._change;
- this.changeService.fetchChangeUpdates(change).then(result => {
+ this.changeModel.fetchChangeUpdates(change).then(result => {
let toastMessage = null;
if (!result.isLatest) {
toastMessage = ReloadToastMessage.NEWER_REVISION;
@@ -2432,8 +2466,8 @@
}
@observe('_patchRange.patchNum')
- _patchNumChanged(patchNumStr: PatchSetNum) {
- if (!this._selectedRevision) {
+ _patchNumChanged(patchNumStr?: PatchSetNum) {
+ if (!this._selectedRevision || !patchNumStr) {
return;
}
assertIsDefined(this._change, '_change');
@@ -2466,7 +2500,7 @@
);
if (editInfo) {
- GerritNav.navigateToChange(this._change, EditPatchSetNum);
+ GerritNav.navigateToChange(this._change, {patchNum: EditPatchSetNum});
return;
}
@@ -2480,21 +2514,30 @@
) {
patchNum = this._patchRange.patchNum;
}
- GerritNav.navigateToChange(this._change, patchNum, undefined, true);
+ GerritNav.navigateToChange(this._change, {
+ patchNum,
+ isEdit: true,
+ forceReload: true,
+ });
}
_handleStopEditTap() {
assertIsDefined(this._change, '_change');
if (!this._patchRange)
throw new Error('missing required _patchRange property');
- GerritNav.navigateToChange(this._change, this._patchRange.patchNum);
+ GerritNav.navigateToChange(this._change, {
+ patchNum: this._patchRange.patchNum,
+ forceReload: true,
+ });
}
_resetReplyOverlayFocusStops() {
- this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops());
+ const dialog = query<GrReplyDialog>(this, '#replyDialog');
+ if (!dialog) return;
+ this.$.replyOverlay.setFocusStops(dialog.getFocusStops());
}
- _handleToggleStar(e: CustomEvent<{change: ChangeInfo; starred: boolean}>) {
+ _handleToggleStar(e: CustomEvent<ChangeStarToggleStarDetail>) {
if (e.detail.starred) {
this.reporting.reportInteraction('change-starred-from-change-view');
this.lastStarredTimestamp = Date.now();
@@ -2572,6 +2615,9 @@
}
declare global {
+ interface HTMLElementEventMap {
+ 'toggle-star': CustomEvent<ChangeStarToggleStarDetail>;
+ }
interface HTMLElementTagNameMap {
'gr-change-view': GrChangeView;
}
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index 0b77bc7..13fb5b3 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
@@ -20,6 +20,9 @@
<style include="gr-a11y-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
+ <style include="gr-paper-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="shared-styles">
.container:not(.loading) {
background-color: var(--background-color-tertiary);
@@ -335,7 +338,7 @@
</div>
<gr-change-star
id="changeStar"
- change="{{_change}}"
+ change="[[_change]]"
on-toggle-star="_handleToggleStar"
hidden$="[[!_loggedIn]]"
></gr-change-star>
@@ -343,7 +346,7 @@
<a
class="changeNumber"
aria-label$="[[_computeChangePermalinkAriaLabel(_change._number)]]"
- href$="[[_computeChangeUrl(_change)]]"
+ href$="[[_computeChangeUrl(_change, 'forceReload')]]"
>[[_change._number]]</a
>
<span class="changeNumberColon">: </span>
@@ -378,7 +381,6 @@
on-stop-edit-tap="_handleStopEditTap"
on-download-tap="_handleOpenDownloadDialog"
on-included-tap="_handleOpenIncludedInDialog"
- comment-threads="[[_commentThreads]]"
></gr-change-actions>
</div>
<!-- end commit actions -->
@@ -436,12 +438,7 @@
</gr-editable-content>
</div>
<h3 class="assistive-tech-only">Comments and Checks Summary</h3>
- <gr-change-summary
- change-comments="[[_changeComments]]"
- comment-threads="[[_commentThreads]]"
- self-account="[[_account]]"
- >
- </gr-change-summary>
+ <gr-change-summary></gr-change-summary>
<gr-endpoint-decorator name="commit-container">
<gr-endpoint-param name="change" value="[[_change]]">
</gr-endpoint-param>
@@ -525,7 +522,6 @@
change="[[_change]]"
change-num="[[_changeNum]]"
revision-info="[[_revisionInfo]]"
- change-comments="[[_changeComments]]"
commit-info="[[_commitInfo]]"
change-url="[[_computeChangeUrl(_change)]]"
edit-mode="[[_editMode]]"
@@ -533,7 +529,6 @@
server-config="[[_serverConfig]]"
shown-file-count="[[_shownFileCount]]"
diff-prefs="[[_diffPrefs]]"
- diff-view-mode="{{viewState.diffMode}}"
patch-num="{{_patchRange.patchNum}}"
base-patch-num="{{_patchRange.basePatchNum}}"
files-expanded="[[_filesExpanded]]"
@@ -551,7 +546,6 @@
change="[[_change]]"
change-num="[[_changeNum]]"
patch-range="{{_patchRange}}"
- change-comments="[[_changeComments]]"
selected-index="{{viewState.selectedFileIndex}}"
diff-view-mode="[[viewState.diffMode]]"
edit-mode="[[_editMode]]"
@@ -571,10 +565,6 @@
<h3 class="assistive-tech-only">Comments</h3>
<gr-thread-list
threads="[[_commentThreads]]"
- change="[[_change]]"
- change-num="[[_changeNum]]"
- logged-in="[[_loggedIn]]"
- account="[[_account]]"
comment-tab-state="[[_tabState.commentTab]]"
only-show-robot-comments-with-human-reply=""
unresolved-only="[[unresolvedOnly]]"
@@ -603,14 +593,7 @@
value="[[_currentRobotCommentsPatchSet]]"
>
</gr-dropdown-list>
- <gr-thread-list
- threads="[[_robotCommentThreads]]"
- change="[[_change]]"
- change-num="[[_changeNum]]"
- logged-in="[[_loggedIn]]"
- hide-dropdown
- empty-thread-msg="[[_messages.NO_ROBOT_COMMENTS_THREADS_MSG]]"
- >
+ <gr-thread-list threads="[[_robotCommentThreads]]" hide-dropdown>
</gr-thread-list>
<template is="dom-if" if="[[_showRobotCommentsButton]]">
<gr-button
@@ -653,14 +636,9 @@
<h2 class="assistive-tech-only">Change Log</h2>
<gr-messages-list
class="hideOnMobileOverlay"
- change="[[_change]]"
- change-num="[[_changeNum]]"
labels="[[_change.labels]]"
messages="[[_change.messages]]"
reviewer-updates="[[_change.reviewer_updates]]"
- change-comments="[[_changeComments]]"
- project-name="[[_change.project]]"
- show-reply-buttons="[[_loggedIn]]"
on-message-anchor-tap="_handleMessageAnchorTap"
on-reply="_handleMessageReply"
></gr-messages-list>
@@ -696,24 +674,26 @@
no-cancel-on-esc-key=""
scroll-action="lock"
with-backdrop=""
+ opened="{{replyOverlayOpened}}"
on-iron-overlay-canceled="onReplyOverlayCanceled"
>
- <gr-reply-dialog
- id="replyDialog"
- change="{{_change}}"
- patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
- permitted-labels="[[_change.permitted_labels]]"
- draft-comment-threads="[[_draftCommentThreads]]"
- project-config="[[_projectConfig]]"
- server-config="[[_serverConfig]]"
- can-be-started="[[_canStartReview]]"
- on-send="_handleReplySent"
- on-cancel="_handleReplyCancel"
- on-autogrow="_handleReplyAutogrow"
- on-send-disabled-changed="_resetReplyOverlayFocusStops"
- hidden$="[[!_loggedIn]]"
- >
- </gr-reply-dialog>
+ <template is="dom-if" if="[[replyOverlayOpened]]">
+ <gr-reply-dialog
+ id="replyDialog"
+ change="{{_change}}"
+ patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
+ permitted-labels="[[_change.permitted_labels]]"
+ draft-comment-threads="[[_draftCommentThreads]]"
+ project-config="[[_projectConfig]]"
+ server-config="[[_serverConfig]]"
+ can-be-started="[[_canStartReview]]"
+ on-send="_handleReplySent"
+ on-cancel="_handleReplyCancel"
+ on-autogrow="_handleReplyAutogrow"
+ on-send-disabled-changed="_resetReplyOverlayFocusStops"
+ hidden$="[[!_loggedIn]]"
+ >
+ </gr-reply-dialog>
+ </template>
</gr-overlay>
- <gr-comment-api id="commentAPI"></gr-comment-api>
`;
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 6cbe59c..4ca593a 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
@@ -17,6 +17,7 @@
import '../../../test/common-test-setup-karma';
import '../../edit/gr-edit-constants';
+import '../gr-thread-list/gr-thread-list';
import './gr-change-view';
import {
ChangeStatus,
@@ -26,16 +27,23 @@
HttpMethod,
MessageTag,
PrimaryTab,
+ createDefaultPreferences,
} from '../../../constants/constants';
import {GrEditConstants} from '../../edit/gr-edit-constants';
import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit';
import {EventType, PluginApi} from '../../../api/plugin';
import 'lodash/lodash';
-import {mockPromise, stubRestApi} from '../../../test/test-utils';
+import {
+ mockPromise,
+ queryAndAssert,
+ stubRestApi,
+ stubUsers,
+ waitQueryAndAssert,
+ waitUntil,
+} from '../../../test/test-utils';
import {
createAppElementChangeViewParams,
createApproval,
@@ -55,6 +63,7 @@
createChangeViewChange,
createRelatedChangeAndCommitInfo,
createAccountDetailWithId,
+ createParsedChange,
} from '../../../test/test-data-generators';
import {ChangeViewPatchRange, GrChangeView} from './gr-change-view';
import {
@@ -64,10 +73,7 @@
ChangeId,
ChangeInfo,
CommitId,
- CommitInfo,
- EditInfo,
EditPatchSetNum,
- GitRef,
NumericChangeId,
ParentPatchSetNum,
PatchRange,
@@ -77,6 +83,7 @@
RevisionInfo,
RevisionPatchSetNum,
RobotId,
+ RobotCommentInfo,
Timestamp,
UrlEncodedCommentId,
} from '../../../types/common';
@@ -88,14 +95,17 @@
import {AppElementChangeViewParams} from '../../gr-app-types';
import {SinonFakeTimers, SinonStubbedMember} from 'sinon';
import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
-import {CommentThread, UIRobot} from '../../../utils/comment-util';
+import {CommentThread} from '../../../utils/comment-util';
import {GerritView} from '../../../services/router/router-model';
import {ParsedChangeInfo} from '../../../types/types';
import {GrRelatedChangesList} from '../gr-related-changes-list/gr-related-changes-list';
-import {appContext} from '../../../services/app-context';
import {ChangeStates} from '../../shared/gr-change-status/gr-change-status';
+import {LoadingStatus} from '../../../services/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';
-const pluginApi = _testOnly_initGerritPluginApi();
const fixture = fixtureFromElement('gr-change-view');
suite('gr-change-view tests', () => {
@@ -149,8 +159,6 @@
message: 'draft',
unresolved: false,
__draft: true,
- __draftID: '0.m683trwff68',
- __editing: false,
patch_set: 2 as PatchSetNum,
},
],
@@ -253,8 +261,6 @@
message: 'resolved draft',
unresolved: false,
__draft: true,
- __draftID: '0.m683trwff68',
- __editing: false,
patch_set: 2 as PatchSetNum,
},
],
@@ -352,7 +358,7 @@
element._changeNum = TEST_NUMERIC_CHANGE_ID;
sinon.stub(element.$.actions, 'reload').returns(Promise.resolve());
getPluginLoader().loadPlugins([]);
- pluginApi.install(
+ window.Gerrit.install(
plugin => {
plugin.registerDynamicCustomComponent(
'change-view-tab-header',
@@ -385,7 +391,7 @@
new CustomEvent('message-anchor-tap', {detail: {id: 'a12345'}})
);
- assert.equal(getUrlStub.lastCall.args[4], '#message-a12345');
+ assert.equal(getUrlStub.lastCall.args[1]!.messageHash, '#message-a12345');
assert.isTrue(replaceStateStub.called);
});
@@ -402,7 +408,7 @@
assert(navigateToChangeStub.called);
const args = navigateToChangeStub.getCall(0).args;
assert.equal(args[0], element._change);
- assert.equal(args[1], 3 as PatchSetNum);
+ assert.equal(args[1]!.patchNum, 3 as PatchSetNum);
});
test('_handleDiffAgainstLatest', () => {
@@ -418,8 +424,8 @@
assert(navigateToChangeStub.called);
const args = navigateToChangeStub.getCall(0).args;
assert.equal(args[0], element._change);
- assert.equal(args[1], 10 as PatchSetNum);
- assert.equal(args[2], 1 as BasePatchSetNum);
+ assert.equal(args[1]!.patchNum, 10 as PatchSetNum);
+ assert.equal(args[1]!.basePatchNum, 1 as BasePatchSetNum);
});
test('_handleDiffBaseAgainstLeft', () => {
@@ -435,7 +441,7 @@
assert(navigateToChangeStub.called);
const args = navigateToChangeStub.getCall(0).args;
assert.equal(args[0], element._change);
- assert.equal(args[1], 1 as PatchSetNum);
+ assert.equal(args[1]!.patchNum, 1 as PatchSetNum);
});
test('_handleDiffRightAgainstLatest', () => {
@@ -450,8 +456,8 @@
element._handleDiffRightAgainstLatest();
assert(navigateToChangeStub.called);
const args = navigateToChangeStub.getCall(0).args;
- assert.equal(args[1], 10 as PatchSetNum);
- assert.equal(args[2], 3 as BasePatchSetNum);
+ assert.equal(args[1]!.patchNum, 10 as PatchSetNum);
+ assert.equal(args[1]!.basePatchNum, 3 as BasePatchSetNum);
});
test('_handleDiffBaseAgainstLatest', () => {
@@ -466,8 +472,8 @@
element._handleDiffBaseAgainstLatest();
assert(navigateToChangeStub.called);
const args = navigateToChangeStub.getCall(0).args;
- assert.equal(args[1], 10 as PatchSetNum);
- assert.isNotOk(args[2]);
+ assert.equal(args[1]!.patchNum, 10 as PatchSetNum);
+ assert.isNotOk(args[1]!.basePatchNum);
});
test('toggle attention set status', async () => {
@@ -545,13 +551,12 @@
test('param change should switch primary tab correctly', async () => {
assert.equal(element._activeTabs[0], PrimaryTab.FILES);
- const queryMap = new Map<string, string>();
- queryMap.set('tab', PrimaryTab.FINDINGS);
// view is required
+ element._changeNum = undefined;
element.params = {
...createAppElementChangeViewParams(),
...element.params,
- queryMap,
+ tab: PrimaryTab.FINDINGS,
};
await flush();
assert.equal(element._activeTabs[0], PrimaryTab.FINDINGS);
@@ -559,13 +564,11 @@
test('invalid param change should not switch primary tab', async () => {
assert.equal(element._activeTabs[0], PrimaryTab.FILES);
- const queryMap = new Map<string, string>();
- queryMap.set('tab', 'random');
// view is required
element.params = {
...createAppElementChangeViewParams(),
...element.params,
- queryMap,
+ tab: 'random',
};
await flush();
assert.equal(element._activeTabs[0], PrimaryTab.FILES);
@@ -660,15 +663,6 @@
messages: createChangeMessages(1),
};
element._change.labels = {};
- stubRestApi('getChangeDetail').callsFake(() =>
- Promise.resolve({
- ...createChangeViewChange(),
- // element has latest info
- revisions: createRevisions(1),
- messages: createChangeMessages(1),
- current_revision: 'rev1' as CommitId,
- })
- );
const openSpy = sinon.spy(element, '_openReplyDialog');
@@ -678,9 +672,7 @@
element.$.replyOverlay.close();
assert.isFalse(element.$.replyOverlay.opened);
assert(
- openSpy.lastCall.calledWithExactly(
- element.$.replyDialog.FocusTarget.ANY
- ),
+ openSpy.lastCall.calledWithExactly(FocusTarget.ANY),
'_openReplyDialog should have been passed ANY'
);
assert.equal(openSpy.callCount, 1);
@@ -702,7 +694,8 @@
},
};
const handlerSpy = sinon.spy(element, '_handleHideBackgroundContent');
- element.$.replyDialog.dispatchEvent(
+ const overlay = queryAndAssert<GrOverlay>(element, '#replyOverlay');
+ overlay.dispatchEvent(
new CustomEvent('fullscreen-overlay-opened', {
composed: true,
bubbles: true,
@@ -729,7 +722,8 @@
},
};
const handlerSpy = sinon.spy(element, '_handleShowBackgroundContent');
- element.$.replyDialog.dispatchEvent(
+ const overlay = queryAndAssert<GrOverlay>(element, '#replyOverlay');
+ overlay.dispatchEvent(
new CustomEvent('fullscreen-overlay-closed', {
composed: true,
bubbles: true,
@@ -803,20 +797,30 @@
assert.isTrue(stub.called);
});
- test('m should toggle diff mode', () => {
- const setModeStub = sinon.stub(
- element.$.fileListHeader,
- 'setDiffViewMode'
+ test('m should toggle diff mode', async () => {
+ const updatePreferencesStub = stubUsers('updatePreferences');
+ await flush();
+
+ const prefs = {
+ ...createDefaultPreferences(),
+ diff_view: DiffViewMode.SIDE_BY_SIDE,
+ };
+ element.userModel.setPreferences(prefs);
+ element._handleToggleDiffMode();
+ assert.isTrue(
+ updatePreferencesStub.calledWith({diff_view: DiffViewMode.UNIFIED})
);
- flush();
- element.viewState.diffMode = DiffViewMode.SIDE_BY_SIDE;
+ const newPrefs = {
+ ...createDefaultPreferences(),
+ diff_view: DiffViewMode.UNIFIED,
+ };
+ element.userModel.setPreferences(newPrefs);
+ await flush();
element._handleToggleDiffMode();
- assert.isTrue(setModeStub.calledWith(DiffViewMode.UNIFIED));
-
- element.viewState.diffMode = DiffViewMode.UNIFIED;
- element._handleToggleDiffMode();
- assert.isTrue(setModeStub.calledWith(DiffViewMode.SIDE_BY_SIDE));
+ assert.isTrue(
+ updatePreferencesStub.calledWith({diff_view: DiffViewMode.SIDE_BY_SIDE})
+ );
});
});
@@ -856,7 +860,43 @@
});
});
- suite('Findings comment tab', () => {
+ suite('Comments tab', () => {
+ setup(async () => {
+ element._changeNum = TEST_NUMERIC_CHANGE_ID;
+ element._change = {
+ ...createChangeViewChange(),
+ revisions: {
+ rev2: createRevision(2),
+ rev1: createRevision(1),
+ rev13: createRevision(13),
+ rev3: createRevision(3),
+ rev4: createRevision(4),
+ },
+ current_revision: 'rev4' as CommitId,
+ };
+ element._commentThreads = THREADS;
+ await flush();
+ const paperTabs = element.shadowRoot!.querySelector('#primaryTabs')!;
+ tap(paperTabs.querySelectorAll('paper-tab')[1]);
+ await flush();
+ });
+
+ test('commentId overrides unresolveOnly default', async () => {
+ const threadList = queryAndAssert<GrThreadList>(
+ element,
+ 'gr-thread-list'
+ );
+ assert.isTrue(element.unresolvedOnly);
+ assert.isNotOk(element.scrollCommentId);
+ assert.isTrue(threadList.unresolvedOnly);
+
+ element.scrollCommentId = 'abcd' as UrlEncodedCommentId;
+ await flush();
+ assert.isFalse(threadList.unresolvedOnly);
+ });
+ });
+
+ suite('Findings robot-comment tab', () => {
setup(async () => {
element._changeNum = TEST_NUMERIC_CHANGE_ID;
element._change = {
@@ -902,11 +942,13 @@
test('only robot comments are rendered', () => {
assert.equal(element._robotCommentThreads!.length, 2);
assert.equal(
- (element._robotCommentThreads![0].comments[0] as UIRobot).robot_id,
+ (element._robotCommentThreads![0].comments[0] as RobotCommentInfo)
+ .robot_id,
'rc1'
);
assert.equal(
- (element._robotCommentThreads![1].comments[0] as UIRobot).robot_id,
+ (element._robotCommentThreads![1].comments[0] as RobotCommentInfo)
+ .robot_id,
'rc2'
);
});
@@ -1006,7 +1048,7 @@
suite('ChangeStatus revert', () => {
test('do not show any chip if no revert created', async () => {
const change = {
- ...createChange(),
+ ...createParsedChange(),
messages: createChangeMessages(2),
};
const getChangeStub = stubRestApi('getChange');
@@ -1036,7 +1078,7 @@
test('do not show any chip if all reverts are abandoned', async () => {
const change = {
- ...createChange(),
+ ...createParsedChange(),
messages: createChangeMessages(2),
};
change.messages[0].message = 'Created a revert of this change as 12345';
@@ -1074,7 +1116,7 @@
test('show revert created if no revert is merged', async () => {
const change = {
- ...createChange(),
+ ...createParsedChange(),
messages: createChangeMessages(2),
};
change.messages[0].message = 'Created a revert of this change as 12345';
@@ -1110,7 +1152,7 @@
test('show revert submitted if revert is merged', async () => {
const change = {
- ...createChange(),
+ ...createParsedChange(),
messages: createChangeMessages(2),
};
change.messages[0].message = 'Created a revert of this change as 12345';
@@ -1241,7 +1283,6 @@
...createChangeViewChange(),
labels: {},
} as ParsedChangeInfo;
- stubRestApi('getChangeDetail').returns(Promise.resolve(change));
element._changeNum = undefined;
element._patchRange = {
basePatchNum: ParentPatchSetNum,
@@ -1278,52 +1319,6 @@
assert.equal(element._numFilesShown, 200);
});
- test('_setDiffViewMode is called with reset when new change is loaded', () => {
- const setDiffViewModeStub = sinon.stub(element, '_setDiffViewMode');
- element.viewState = {changeNum: 1 as NumericChangeId};
- element._changeNum = 2 as NumericChangeId;
- element._resetFileListViewState();
- assert.isTrue(setDiffViewModeStub.calledWithExactly(true));
- });
-
- test('diffViewMode is propagated from file list header', () => {
- element.viewState = {diffMode: DiffViewMode.UNIFIED};
- element.$.fileListHeader.diffViewMode = DiffViewMode.SIDE_BY_SIDE;
- assert.equal(element.viewState.diffMode, DiffViewMode.SIDE_BY_SIDE);
- });
-
- test('diffMode defaults to side by side without preferences', async () => {
- stubRestApi('getPreferences').returns(Promise.resolve(createPreferences()));
- // No user prefs or diff view mode set.
-
- await element._setDiffViewMode()!;
- assert.equal(element.viewState.diffMode, DiffViewMode.SIDE_BY_SIDE);
- });
-
- test('diffMode defaults to preference when not already set', async () => {
- stubRestApi('getPreferences').returns(
- Promise.resolve({
- ...createPreferences(),
- default_diff_view: DiffViewMode.UNIFIED,
- })
- );
-
- await element._setDiffViewMode()!;
- assert.equal(element.viewState.diffMode, DiffViewMode.UNIFIED);
- });
-
- test('existing diffMode overrides preference', async () => {
- element.viewState.diffMode = DiffViewMode.SIDE_BY_SIDE;
- stubRestApi('getPreferences').returns(
- Promise.resolve({
- ...createPreferences(),
- default_diff_view: DiffViewMode.UNIFIED,
- })
- );
- await element._setDiffViewMode()!;
- assert.equal(element.viewState.diffMode, DiffViewMode.SIDE_BY_SIDE);
- });
-
test('don’t reload entire page when patchRange changes', async () => {
const reloadStub = sinon
.stub(element, 'loadData')
@@ -1333,12 +1328,12 @@
.callsFake(() => Promise.resolve([undefined, undefined, undefined]));
flush();
const collapseStub = sinon.stub(element.$.fileList, 'collapseAllDiffs');
-
const value: AppElementChangeViewParams = {
...createAppElementChangeViewParams(),
view: GerritView.CHANGE,
patchNum: 1 as RevisionPatchSetNum,
};
+ element._changeNum = undefined;
element.params = value;
await flush();
assert.isTrue(reloadStub.calledOnce);
@@ -1363,13 +1358,17 @@
test('reload ported comments when patchNum changes', async () => {
sinon.stub(element, 'loadData').callsFake(() => Promise.resolve());
- sinon.stub(element, '_getCommitInfo');
+ sinon.stub(element, 'loadAndSetCommitInfo');
sinon.stub(element.$.fileList, 'reload');
flush();
const reloadPortedCommentsStub = sinon.stub(
- element.$.commentAPI,
+ element.getCommentsModel(),
'reloadPortedComments'
);
+ const reloadPortedDraftsStub = sinon.stub(
+ element.getCommentsModel(),
+ 'reloadPortedDrafts'
+ );
sinon.stub(element.$.fileList, 'collapseAllDiffs');
const value: AppElementChangeViewParams = {
@@ -1394,9 +1393,10 @@
element.params = {...value};
await flush();
assert.isTrue(reloadPortedCommentsStub.calledOnce);
+ assert.isTrue(reloadPortedDraftsStub.calledOnce);
});
- test('reload entire page when patchRange doesnt change', async () => {
+ test('do not reload entire page when patchRange doesnt change', async () => {
const reloadStub = sinon
.stub(element, 'loadData')
.callsFake(() => Promise.resolve());
@@ -1404,13 +1404,32 @@
const value: AppElementChangeViewParams =
createAppElementChangeViewParams();
element.params = value;
+ // change already loaded
+ assert.isOk(element._changeNum);
await flush();
- assert.isTrue(reloadStub.calledOnce);
+ assert.isFalse(reloadStub.calledOnce);
element._initialLoadComplete = true;
element.params = {...value};
await flush();
- assert.isTrue(reloadStub.calledTwice);
- assert.isTrue(collapseStub.calledTwice);
+ assert.isFalse(reloadStub.calledTwice);
+ assert.isFalse(collapseStub.calledTwice);
+ });
+
+ test('forceReload updates the change', async () => {
+ const getChangeStub = stubRestApi('getChangeDetail').returns(
+ Promise.resolve(createParsedChange())
+ );
+ const loadDataStub = sinon
+ .stub(element, 'loadData')
+ .callsFake(() => Promise.resolve());
+ const collapseStub = sinon.stub(element.$.fileList, 'collapseAllDiffs');
+ element.params = {...createAppElementChangeViewParams(), forceReload: true};
+ await flush();
+ assert.isTrue(getChangeStub.called);
+ assert.isTrue(loadDataStub.called);
+ assert.isTrue(collapseStub.called);
+ // patchNum is set by changeChanged, so this verifies that _change was set.
+ assert.isOk(element._patchRange?.patchNum);
});
test('do not handle new change numbers', async () => {
@@ -1521,69 +1540,34 @@
test('topic is coalesced to null', async () => {
sinon.stub(element, '_changeChanged');
- stubRestApi('getChangeDetail').returns(
- Promise.resolve({
+ element.changeModel.setState({
+ loadingStatus: LoadingStatus.LOADED,
+ change: {
...createChangeViewChange(),
labels: {},
current_revision: 'foo' as CommitId,
revisions: {foo: createRevision()},
- })
- );
+ },
+ });
- await element._getChangeDetail();
+ await element.performPostChangeLoadTasks();
assert.isNull(element._change!.topic);
});
test('commit sha is populated from getChangeDetail', async () => {
sinon.stub(element, '_changeChanged');
- stubRestApi('getChangeDetail').callsFake(() =>
- Promise.resolve({
+ element.changeModel.setState({
+ loadingStatus: LoadingStatus.LOADED,
+ change: {
...createChangeViewChange(),
labels: {},
current_revision: 'foo' as CommitId,
revisions: {foo: createRevision()},
- })
- );
-
- await element._getChangeDetail();
- assert.equal('foo', element._commitInfo!.commit);
- });
-
- test('edit is added to change', () => {
- sinon.stub(element, '_changeChanged');
- const changeRevision = createRevision();
- stubRestApi('getChangeDetail').callsFake(() =>
- Promise.resolve({
- ...createChangeViewChange(),
- labels: {},
- current_revision: 'foo' as CommitId,
- revisions: {foo: {...changeRevision}},
- })
- );
- const editCommit: CommitInfo = {
- ...createCommit(),
- commit: 'bar' as CommitId,
- };
- sinon.stub(element, '_getEdit').callsFake(() =>
- Promise.resolve({
- base_patch_set_number: 1 as BasePatchSetNum,
- commit: {...editCommit},
- base_revision: 'abc',
- ref: 'some/ref' as GitRef,
- })
- );
- element._patchRange = {};
-
- return element._getChangeDetail().then(() => {
- const revs = element._change!.revisions!;
- assert.equal(Object.keys(revs).length, 2);
- assert.deepEqual(revs['foo'], changeRevision);
- assert.deepEqual(revs['bar'], {
- ...createEditRevision(),
- commit: editCommit,
- fetch: undefined,
- });
+ },
});
+
+ await element.performPostChangeLoadTasks();
+ assert.equal('foo', element._commitInfo!.commit);
});
test('_getBasePatchNum', () => {
@@ -1635,9 +1619,7 @@
const openStub = sinon.stub(element, '_openReplyDialog');
tap(element.$.replyBtn);
assert(
- openStub.lastCall.calledWithExactly(
- element.$.replyDialog.FocusTarget.ANY
- ),
+ openStub.lastCall.calledWithExactly(FocusTarget.ANY),
'_openReplyDialog should have been passed ANY'
);
assert.equal(openStub.callCount, 1);
@@ -1656,18 +1638,12 @@
bubbles: true,
})
);
- assert(
- openStub.lastCall.calledWithExactly(
- element.$.replyDialog.FocusTarget.BODY
- ),
- '_openReplyDialog should have been passed BODY'
- );
- assert.equal(openStub.callCount, 1);
+ assert.isTrue(openStub.calledOnce);
+ assert.equal(openStub.lastCall.args[0], FocusTarget.BODY);
}
);
test('reply dialog focus can be controlled', () => {
- const FocusTarget = element.$.replyDialog.FocusTarget;
const openStub = sinon.stub(element, '_openReplyDialog');
const e = new CustomEvent('show-reply-dialog', {
@@ -1743,22 +1719,14 @@
suite('reply dialog tests', () => {
setup(() => {
- sinon.stub(element.$.replyDialog, '_draftChanged');
element._change = {
...createChangeViewChange(),
- revisions: createRevisions(1),
+ // element has latest info
+ revisions: {rev1: createRevision()},
messages: createChangeMessages(1),
+ current_revision: 'rev1' as CommitId,
+ labels: {},
};
- element._change.labels = {};
- stubRestApi('getChangeDetail').callsFake(() =>
- Promise.resolve({
- ...createChangeViewChange(),
- // element has latest info
- revisions: {rev1: createRevision()},
- messages: createChangeMessages(1),
- current_revision: 'rev1' as CommitId,
- })
- );
});
test('show reply dialog on open-reply-dialog event', async () => {
@@ -1774,52 +1742,19 @@
assert.isTrue(openReplyDialogStub.calledOnce);
});
- test('reply from comment adds quote text', () => {
+ test('reply from comment adds quote text', async () => {
const e = new CustomEvent('', {
detail: {message: {message: 'quote text'}},
});
element._handleMessageReply(e);
- assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
- });
-
- test('reply from comment replaces quote text', () => {
- element.$.replyDialog.draft = '> old quote text\n\n some draft text';
- element.$.replyDialog.quote = '> old quote text\n\n';
- const e = new CustomEvent('', {
- detail: {message: {message: 'quote text'}},
- });
- element._handleMessageReply(e);
- assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
- });
-
- test('reply from same comment preserves quote text', () => {
- element.$.replyDialog.draft = '> quote text\n\n some draft text';
- element.$.replyDialog.quote = '> quote text\n\n';
- const e = new CustomEvent('', {
- detail: {message: {message: 'quote text'}},
- });
- element._handleMessageReply(e);
- assert.equal(
- element.$.replyDialog.draft,
- '> quote text\n\n some draft text'
+ const dialog = await waitQueryAndAssert<GrReplyDialog>(
+ element,
+ '#replyDialog'
);
- assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
- });
-
- test('reply from top of page contains previous draft', () => {
- const div = document.createElement('div');
- element.$.replyDialog.draft = '> quote text\n\n some draft text';
- element.$.replyDialog.quote = '> quote text\n\n';
- const e = {
- target: div,
- preventDefault: sinon.spy(),
- } as unknown as MouseEvent;
- element._handleReplyTap(e);
- assert.equal(
- element.$.replyDialog.draft,
- '> quote text\n\n some draft text'
- );
- assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
+ const openSpy = sinon.spy(dialog, 'open');
+ await flush();
+ await waitUntil(() => openSpy.called && !!openSpy.lastCall.args[1]);
+ assert.equal(openSpy.lastCall.args[1], '> quote text\n\n');
});
});
@@ -1893,50 +1828,33 @@
...createChangeViewChange(),
current_revision: 'foo' as CommitId,
revisions: {
- foo: {...createRevision(), actions: {cherrypick: {enabled: true}}},
+ foo: {...createRevision()},
},
};
- let mockChange;
- // With no edit, mockChange should be unmodified.
- element._processEdit((mockChange = _.cloneDeep(change)), false);
- assert.deepEqual(mockChange, change);
+ // With no edit, nothing happens.
+ element._processEdit(change);
+ assert.equal(element._patchRange.patchNum, undefined);
- const editCommit: CommitInfo = {
- ...createCommit(),
- commit: 'bar' as CommitId,
- };
- // When edit is not based on the latest PS, current_revision should be
- // unmodified.
- const edit: EditInfo = {
- ref: 'ref/test/abc' as GitRef,
- base_revision: 'abc',
- base_patch_set_number: 1 as BasePatchSetNum,
- commit: {...editCommit},
+ change.revisions['bar'] = {
+ _number: EditPatchSetNum,
+ basePatchNum: 1 as BasePatchSetNum,
+ commit: {
+ ...createCommit(),
+ commit: 'bar' as CommitId,
+ },
fetch: {},
};
- element._processEdit((mockChange = _.cloneDeep(change)), edit);
- assert.notDeepEqual(mockChange, change);
- assert.equal(mockChange.revisions.bar._number, EditPatchSetNum);
- assert.equal(mockChange.current_revision, change.current_revision);
- assert.deepEqual(mockChange.revisions.bar.commit, editCommit);
- assert.notOk(mockChange.revisions.bar.actions);
- edit.base_revision = 'foo';
- element._processEdit((mockChange = _.cloneDeep(change)), edit);
- assert.notDeepEqual(mockChange, change);
- assert.equal(mockChange.current_revision, 'bar');
- assert.deepEqual(
- mockChange.revisions.bar.actions,
- mockChange.revisions.foo.actions
- );
+ // When edit is set, but not patchNum, then switch to edit ps.
+ element._processEdit(change);
+ assert.equal(element._patchRange.patchNum, EditPatchSetNum);
- // If _patchRange.patchNum is defined, do not load edit.
+ // When edit is set, but patchNum as well, then keep patchNum.
element._patchRange.patchNum = 5 as RevisionPatchSetNum;
- change.current_revision = 'baz' as CommitId;
- element._processEdit((mockChange = _.cloneDeep(change)), edit);
+ element.routerPatchNum = 5 as RevisionPatchSetNum;
+ element._processEdit(change);
assert.equal(element._patchRange.patchNum, 5 as RevisionPatchSetNum);
- assert.notOk(mockChange.revisions.bar.actions);
});
test('file-action-tap handling', () => {
@@ -2021,8 +1939,9 @@
test('_selectedRevision updates when patchNum is changed', () => {
const revision1: RevisionInfo = createRevision(1);
const revision2: RevisionInfo = createRevision(2);
- stubRestApi('getChangeDetail').returns(
- Promise.resolve({
+ element.changeModel.setState({
+ loadingStatus: LoadingStatus.LOADED,
+ change: {
...createChangeViewChange(),
revisions: {
aaa: revision1,
@@ -2031,14 +1950,14 @@
labels: {},
actions: {},
current_revision: 'bbb' as CommitId,
- })
- );
- sinon.stub(element, '_getEdit').returns(Promise.resolve(false));
+ },
+ });
+
sinon
.stub(element, '_getPreferences')
.returns(Promise.resolve(createPreferences()));
element._patchRange = {patchNum: 2 as RevisionPatchSetNum};
- return element._getChangeDetail().then(() => {
+ return element.performPostChangeLoadTasks().then(() => {
assert.strictEqual(element._selectedRevision, revision2);
element.set('_patchRange.patchNum', '1');
@@ -2046,12 +1965,13 @@
});
});
- test('_selectedRevision is assigned when patchNum is edit', () => {
+ test('_selectedRevision is assigned when patchNum is edit', async () => {
const revision1 = createRevision(1);
const revision2 = createRevision(2);
const revision3 = createEditRevision();
- stubRestApi('getChangeDetail').returns(
- Promise.resolve({
+ element.changeModel.setState({
+ loadingStatus: LoadingStatus.LOADED,
+ change: {
...createChangeViewChange(),
revisions: {
aaa: revision1,
@@ -2061,16 +1981,14 @@
labels: {},
actions: {},
current_revision: 'ccc' as CommitId,
- })
- );
- sinon.stub(element, '_getEdit').returns(Promise.resolve(undefined));
+ },
+ });
sinon
.stub(element, '_getPreferences')
.returns(Promise.resolve(createPreferences()));
element._patchRange = {patchNum: EditPatchSetNum};
- return element._getChangeDetail().then(() => {
- assert.strictEqual(element._selectedRevision, revision3);
- });
+ await element.performPostChangeLoadTasks();
+ assert.strictEqual(element._selectedRevision, revision3);
});
test('_sendShowChangeEvent', () => {
@@ -2078,7 +1996,7 @@
element._change = {...change};
element._patchRange = {patchNum: 4 as RevisionPatchSetNum};
element._mergeable = true;
- const showStub = sinon.stub(appContext.jsApiService, 'handleEvent');
+ const showStub = sinon.stub(element.jsAPI, 'handleEvent');
element._sendShowChangeEvent();
assert.isTrue(showStub.calledOnce);
assert.equal(showStub.lastCall.args[0], EventType.SHOW_CHANGE);
@@ -2089,6 +2007,39 @@
});
});
+ test('patch range changed', () => {
+ element._patchRange = undefined;
+ element._change = createChangeViewChange();
+ element._change!.revisions = createRevisions(4);
+ element._change.current_revision = '1' as CommitId;
+ element._change = {...element._change};
+
+ const params = createAppElementChangeViewParams();
+
+ assert.isFalse(element.hasPatchRangeChanged(params));
+ assert.isFalse(element.hasPatchNumChanged(params));
+
+ params.basePatchNum = ParentPatchSetNum;
+ // undefined means navigate to latest patchset
+ params.patchNum = undefined;
+
+ element._patchRange = {
+ patchNum: 2 as RevisionPatchSetNum,
+ basePatchNum: ParentPatchSetNum,
+ };
+
+ assert.isTrue(element.hasPatchRangeChanged(params));
+ assert.isTrue(element.hasPatchNumChanged(params));
+
+ element._patchRange = {
+ patchNum: 4 as RevisionPatchSetNum,
+ basePatchNum: ParentPatchSetNum,
+ };
+
+ assert.isFalse(element.hasPatchRangeChanged(params));
+ assert.isFalse(element.hasPatchNumChanged(params));
+ });
+
suite('_handleEditTap', () => {
let fireEdit: () => void;
@@ -2108,7 +2059,7 @@
const promise = mockPromise();
sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
assert.equal(args.length, 2);
- assert.equal(args[1], EditPatchSetNum); // patchNum
+ assert.equal(args[1]!.patchNum, EditPatchSetNum); // patchNum
promise.resolve();
});
@@ -2124,9 +2075,9 @@
test('no edit exists in revisions, non-latest patchset', async () => {
const promise = mockPromise();
sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
- assert.equal(args.length, 4);
- assert.equal(args[1], 1 as PatchSetNum); // patchNum
- assert.equal(args[3], true); // opt_isEdit
+ assert.equal(args.length, 2);
+ assert.equal(args[1]!.patchNum, 1 as PatchSetNum); // patchNum
+ assert.equal(args[1]!.isEdit, true); // opt_isEdit
promise.resolve();
});
@@ -2141,10 +2092,10 @@
test('no edit exists in revisions, latest patchset', async () => {
const promise = mockPromise();
sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
- assert.equal(args.length, 4);
+ assert.equal(args.length, 2);
// No patch should be specified when patchNum == latest.
- assert.isNotOk(args[1]); // patchNum
- assert.equal(args[3], true); // opt_isEdit
+ assert.isNotOk(args[1]!.patchNum); // patchNum
+ assert.equal(args[1]!.isEdit, true); // opt_isEdit
promise.resolve();
});
@@ -2166,7 +2117,7 @@
const promise = mockPromise();
sinon.stub(GerritNav, 'navigateToChange').callsFake((...args) => {
assert.equal(args.length, 2);
- assert.equal(args[1], 1 as PatchSetNum); // patchNum
+ assert.equal(args[1]!.patchNum, 1 as PatchSetNum); // patchNum
promise.resolve();
});
@@ -2182,7 +2133,11 @@
element._change = {...createChangeViewChange(), labels: {}};
element._selectedRevision = createRevision();
const promise = mockPromise();
- pluginApi.install(promise.resolve, '0.1', 'http://some/plugins/url.js');
+ window.Gerrit.install(
+ promise.resolve,
+ '0.1',
+ 'http://some/plugins/url.js'
+ );
await flush();
const plugin: PluginApi = (await promise) as PluginApi;
const hookEl = await plugin
@@ -2230,17 +2185,19 @@
});
});
- test('_handleToggleStar called when star is tapped', () => {
+ test('_handleToggleStar called when star is tapped', async () => {
element._change = {
...createChangeViewChange(),
owner: {_account_id: 1 as AccountId},
starred: false,
};
element._loggedIn = true;
- const stub = sinon.stub(element, '_handleToggleStar');
- flush();
+ await flush();
- tap(element.$.changeStar.shadowRoot!.querySelector('button')!);
+ const stub = sinon.stub(element, '_handleToggleStar');
+
+ const changeStar = queryAndAssert<GrChangeStar>(element, '#changeStar');
+ tap(queryAndAssert<HTMLButtonElement>(changeStar, 'button')!);
assert.isTrue(stub.called);
});
@@ -2250,7 +2207,9 @@
basePatchNum: ParentPatchSetNum,
patchNum: 1 as RevisionPatchSetNum,
};
- sinon.stub(element, '_getChangeDetail').returns(Promise.resolve(false));
+ sinon
+ .stub(element, 'performPostChangeLoadTasks')
+ .returns(Promise.resolve(false));
sinon.stub(element, '_getProjectConfig').returns(Promise.resolve());
sinon.stub(element, '_getMergeability').returns(Promise.resolve());
sinon.stub(element, '_getLatestCommitMessage').returns(Promise.resolve());
@@ -2261,11 +2220,11 @@
test("don't report changeDisplayed on reply", async () => {
const changeDisplayStub = sinon.stub(
- appContext.reportingService,
+ element.reporting,
'changeDisplayed'
);
const changeFullyLoadedStub = sinon.stub(
- appContext.reportingService,
+ element.reporting,
'changeFullyLoaded'
);
element._handleReplySent();
@@ -2276,18 +2235,29 @@
test('report changeDisplayed on _paramsChanged', async () => {
const changeDisplayStub = sinon.stub(
- appContext.reportingService,
+ element.reporting,
'changeDisplayed'
);
const changeFullyLoadedStub = sinon.stub(
- appContext.reportingService,
+ element.reporting,
'changeFullyLoaded'
);
+ // reset so reload is triggered
+ element._changeNum = undefined;
element.params = {
...createAppElementChangeViewParams(),
changeNum: TEST_NUMERIC_CHANGE_ID,
project: TEST_PROJECT_NAME,
};
+ element.changeModel.setState({
+ loadingStatus: LoadingStatus.LOADED,
+ change: {
+ ...createChangeViewChange(),
+ labels: {},
+ current_revision: 'foo' as CommitId,
+ revisions: {foo: createRevision()},
+ },
+ });
await flush();
assert.isTrue(changeDisplayStub.called);
assert.isTrue(changeFullyLoadedStub.called);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
index b061043..56b9f11 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
@@ -22,7 +22,7 @@
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-confirm-cherrypick-dialog_html';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {
ChangeInfo,
BranchName,
@@ -30,7 +30,6 @@
CommitId,
ChangeInfoId,
} from '../../../types/common';
-import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
import {customElement, property, observe} from '@polymer/decorators';
import {GrTypedAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
import {HttpMethod, ChangeStatus} from '../../../constants/constants';
@@ -135,17 +134,15 @@
@property({type: Boolean})
_invalidBranch = false;
- @property({type: Object})
- reporting: ReportingService;
-
private selectedChangeIds = new Set<ChangeInfoId>();
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
+
+ private readonly reporting = getAppContext().reportingService;
constructor() {
super();
this._statuses = {};
- this.reporting = appContext.reportingService;
this._query = (text: string) => this._getProjectBranchesSuggestions(text);
}
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
index b3bbc8a..c34c577 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
@@ -21,7 +21,7 @@
import {htmlTemplate} from './gr-confirm-move-dialog_html';
import {customElement, property} from '@polymer/decorators';
import {BranchName, RepoName} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {GrTypedAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
import {addShortcut, Key, Modifier} from '../../../utils/dom-util';
@@ -88,7 +88,7 @@
);
}
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
index 77b7717..00f65b0 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
@@ -26,7 +26,7 @@
AutocompleteQuery,
AutocompleteSuggestion,
} from '../../shared/gr-autocomplete/gr-autocomplete';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
export interface RebaseChange {
@@ -91,7 +91,7 @@
@property({type: Array})
_recentChanges?: RebaseChange[];
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
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 b971039..e0532c2 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
@@ -22,10 +22,11 @@
import {customElement, property} from '@polymer/decorators';
import {ChangeInfo, CommitId} from '../../../types/common';
import {fireAlert} from '../../../utils/event-util';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
const ERR_COMMIT_NOT_FOUND = 'Unable to find the commit hash of this change.';
const CHANGE_SUBJECT_LIMIT = 50;
+const INSERT_REASON_STRING = '<INSERT REASONING HERE>';
// TODO(dhruvsri): clean up repeated definitions after moving to js modules
export enum RevertType {
@@ -83,7 +84,7 @@
@property({type: Array})
_revertMessages: string[] = [];
- private readonly jsAPI = appContext.jsApiService;
+ private readonly jsAPI = getAppContext().jsApiService;
_computeIfSingleRevert(revertType: number) {
return revertType === RevertType.REVERT_SINGLE_CHANGE;
@@ -124,7 +125,7 @@
const message =
`${revertTitle}\n\n${revertCommitText}\n\n` +
- 'Reason for revert: <INSERT REASONING HERE>\n';
+ `Reason for revert: ${INSERT_REASON_STRING}\n`;
// This is to give plugins a chance to update message
this._message = this._modifyRevertMsg(change, commitMessage, message);
this._revertType = RevertType.REVERT_SINGLE_CHANGE;
@@ -201,7 +202,10 @@
_handleConfirmTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- if (this._message === this._originalRevertMessages[this._revertType]) {
+ if (
+ this._message === this._originalRevertMessages[this._revertType] ||
+ this._message.includes(INSERT_REASON_STRING)
+ ) {
this._showErrorMessage = true;
return;
}
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
index 9d371d3..aeff1c7 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
@@ -20,14 +20,19 @@
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
import '../gr-thread-list/gr-thread-list';
-import {ChangeInfo, ActionInfo} from '../../../types/common';
+import {ActionInfo} from '../../../types/common';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {pluralize} from '../../../utils/string-util';
import {CommentThread, isUnresolved} from '../../../utils/comment-util';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, css, html} from 'lit';
-import {customElement, property, query} from 'lit/decorators';
+import {customElement, property, query, state} from 'lit/decorators';
import {fontStyles} from '../../../styles/gr-font-styles';
+import {subscribe} from '../../lit/subscription-controller';
+import {getAppContext} from '../../../services/app-context';
+import {ParsedChangeInfo} from '../../../types/types';
+import {commentsModelToken} from '../../../models/comments/comments-model';
+import {resolve} from '../../../models/dependency';
@customElement('gr-confirm-submit-dialog')
export class GrConfirmSubmitDialog extends LitElement {
@@ -47,16 +52,20 @@
*/
@property({type: Object})
- change?: ChangeInfo;
-
- @property({type: Object})
action?: ActionInfo;
- @property({type: Array})
- commentThreads?: CommentThread[] = [];
+ @state()
+ change?: ParsedChangeInfo;
- @property({type: Boolean})
- _initialised = false;
+ @state()
+ unresolvedThreads: CommentThread[] = [];
+
+ @state()
+ initialised = false;
+
+ private getCommentsModel = resolve(this, commentsModelToken);
+
+ private changeModel = getAppContext().changeModel;
static override get styles() {
return [
@@ -84,6 +93,16 @@
];
}
+ override connectedCallback() {
+ super.connectedCallback();
+ subscribe(this, this.changeModel.change$, x => (this.change = x));
+ subscribe(
+ this,
+ this.getCommentsModel().threads$,
+ x => (this.unresolvedThreads = x.filter(isUnresolved))
+ );
+ }
+
private renderPrivate() {
if (!this.change?.is_private) return '';
return html`
@@ -99,21 +118,18 @@
}
private renderUnresolvedCommentCount() {
- if (!this.change?.unresolved_comment_count) return '';
+ if (!this.unresolvedThreads?.length) return '';
return html`
<p>
<iron-icon
icon="gr-icons:warning"
class="warningBeforeSubmit"
></iron-icon>
- ${this._computeUnresolvedCommentsWarning(this.change)}
+ ${this.computeUnresolvedCommentsWarning()}
</p>
<gr-thread-list
id="commentList"
- .threads="${this._computeUnresolvedThreads(this.commentThreads)}"
- .change="${this.change}"
- .changeNum="${this.change?._number}"
- logged-in
+ .threads="${this.unresolvedThreads}"
hide-dropdown
>
</gr-thread-list>
@@ -121,7 +137,7 @@
}
private renderChangeEdit() {
- if (!this._computeHasChangeEdit(this.change)) return '';
+ if (!this.computeHasChangeEdit()) return '';
return html`
<iron-icon
icon="gr-icons:warning"
@@ -133,7 +149,7 @@
}
private renderInitialised() {
- if (!this._initialised) return '';
+ if (!this.initialised) return '';
return html`
<div class="header" slot="header">${this.action?.label}</div>
<div class="main" slot="main">
@@ -159,48 +175,42 @@
id="dialog"
confirm-label="Continue"
confirm-on-enter=""
- @cancel=${this._handleCancelTap}
- @confirm=${this._handleConfirmTap}
+ @cancel=${this.handleCancelTap}
+ @confirm=${this.handleConfirmTap}
>
${this.renderInitialised()}
</gr-dialog>`;
}
init() {
- this._initialised = true;
+ this.initialised = true;
}
resetFocus() {
this.dialog?.resetFocus();
}
- _computeHasChangeEdit(change?: ChangeInfo) {
- return (
- !!change &&
- !!change.revisions &&
- Object.values(change.revisions).some(rev => rev._number === 'edit')
+ // Private method, but visible for testing.
+ computeHasChangeEdit() {
+ return Object.values(this.change?.revisions ?? {}).some(
+ rev => rev._number === 'edit'
);
}
- _computeUnresolvedThreads(commentThreads?: CommentThread[]) {
- if (!commentThreads) return [];
- return commentThreads.filter(thread => isUnresolved(thread));
- }
-
- _computeUnresolvedCommentsWarning(change?: ChangeInfo) {
- if (!change) return '';
- const unresolvedCount = change.unresolved_comment_count;
+ // Private method, but visible for testing.
+ computeUnresolvedCommentsWarning() {
+ const unresolvedCount = this.unresolvedThreads.length;
if (!unresolvedCount) throw new Error('unresolved comments undefined or 0');
return `Heads Up! ${pluralize(unresolvedCount, 'unresolved comment')}.`;
}
- _handleConfirmTap(e: Event) {
+ private handleConfirmTap(e: Event) {
e.preventDefault();
e.stopPropagation();
this.dispatchEvent(new CustomEvent('confirm', {bubbles: false}));
}
- _handleCancelTap(e: Event) {
+ private handleCancelTap(e: Event) {
e.preventDefault();
e.stopPropagation();
this.dispatchEvent(new CustomEvent('cancel', {bubbles: false}));
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.ts
index e1823b1..9962351 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.ts
@@ -16,10 +16,15 @@
*/
import '../../../test/common-test-setup-karma';
-import {createChange, createRevision} from '../../../test/test-data-generators';
+import {
+ createParsedChange,
+ createRevision,
+ createThread,
+} from '../../../test/test-data-generators';
import {queryAndAssert} from '../../../test/test-utils';
import {PatchSetNum} from '../../../types/common';
import {GrConfirmSubmitDialog} from './gr-confirm-submit-dialog';
+import './gr-confirm-submit-dialog';
const basicFixture = fixtureFromElement('gr-confirm-submit-dialog');
@@ -28,17 +33,17 @@
setup(() => {
element = basicFixture.instantiate();
- element._initialised = true;
+ element.initialised = true;
});
test('display', async () => {
element.action = {label: 'my-label'};
element.change = {
- ...createChange(),
+ ...createParsedChange(),
subject: 'my-subject',
revisions: {},
};
- await flush();
+ await element.updateComplete;
const header = queryAndAssert(element, '.header');
assert.equal(header.textContent!.trim(), 'my-label');
@@ -47,23 +52,24 @@
assert.notEqual(message.textContent!.indexOf('my-subject'), -1);
});
- test('_computeUnresolvedCommentsWarning', () => {
- const change = {...createChange(), unresolved_comment_count: 1};
+ test('computeUnresolvedCommentsWarning', () => {
+ element.change = {...createParsedChange()};
+ element.unresolvedThreads = [createThread()];
assert.equal(
- element._computeUnresolvedCommentsWarning(change),
+ element.computeUnresolvedCommentsWarning(),
'Heads Up! 1 unresolved comment.'
);
- const change2 = {...createChange(), unresolved_comment_count: 2};
+ element.unresolvedThreads = [...element.unresolvedThreads, createThread()];
assert.equal(
- element._computeUnresolvedCommentsWarning(change2),
+ element.computeUnresolvedCommentsWarning(),
'Heads Up! 2 unresolved comments.'
);
});
- test('_computeHasChangeEdit', () => {
- const change = {
- ...createChange(),
+ test('computeHasChangeEdit', () => {
+ element.change = {
+ ...createParsedChange(),
revisions: {
d442ff05d6c4f2a3af0eeca1f67374b39f9dc3d8: {
...createRevision(),
@@ -73,10 +79,10 @@
unresolved_comment_count: 0,
};
- assert.isTrue(element._computeHasChangeEdit(change));
+ assert.isTrue(element.computeHasChangeEdit());
- const change2 = {
- ...createChange(),
+ element.change = {
+ ...createParsedChange(),
revisions: {
d442ff05d6c4f2a3af0eeca1f67374b39f9dc3d8: {
...createRevision(),
@@ -84,6 +90,6 @@
},
},
};
- assert.isFalse(element._computeHasChangeEdit(change2));
+ assert.isFalse(element.computeHasChangeEdit());
});
});
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 ae3eee5..766b0c3 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
@@ -39,16 +39,14 @@
BasePatchSetNum,
} from '../../../types/common';
import {DiffPreferencesInfo} from '../../../types/diff';
-import {ChangeComments} from '../../diff/gr-comment-api/gr-comment-api';
import {GrDiffModeSelector} from '../../diff/gr-diff-mode-selector/gr-diff-mode-selector';
-import {DiffViewMode} from '../../../constants/constants';
import {GrButton} from '../../shared/gr-button/gr-button';
import {fireEvent} from '../../../utils/event-util';
import {
Shortcut,
ShortcutSection,
} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
declare global {
interface HTMLElementTagNameMap {
@@ -102,9 +100,6 @@
changeUrl?: string;
@property({type: Object})
- changeComments?: ChangeComments;
-
- @property({type: Object})
commitInfo?: CommitInfo;
@property({type: Boolean})
@@ -122,9 +117,6 @@
@property({type: Object})
diffPrefs?: DiffPreferencesInfo;
- @property({type: String, notify: true})
- diffViewMode?: DiffViewMode;
-
@property({type: String})
patchNum?: PatchSetNum;
@@ -142,11 +134,7 @@
@property({type: Object})
revisionInfo?: RevisionInfo;
- private readonly shortcuts = appContext.shortcutsService;
-
- setDiffViewMode(mode: DiffViewMode) {
- this.$.modeSelect.setMode(mode);
- }
+ private readonly shortcuts = getAppContext().shortcutsService;
_expandAllDiffs() {
fireEvent(this, 'expand-diffs');
@@ -187,7 +175,7 @@
) {
return;
}
- GerritNav.navigateToChange(this.change, patchNum, basePatchNum);
+ GerritNav.navigateToChange(this.change, {patchNum, basePatchNum});
}
_handlePrefsTap(e: Event) {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
index 73d0819..fbba2fc 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
@@ -132,7 +132,6 @@
<div class="patchInfoContent">
<gr-patch-range-select
id="rangeSelect"
- change-comments="[[changeComments]]"
change-num="[[changeNum]]"
patch-num="[[patchNum]]"
base-patch-num="[[basePatchNum]]"
@@ -169,7 +168,6 @@
<span class="fileViewActionsLabel">Diff view:</span>
<gr-diff-mode-selector
id="modeSelect"
- mode="{{diffViewMode}}"
save-on-change="[[loggedIn]]"
></gr-diff-mode-selector>
<span
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
index 479a9a1..ac9e8c0 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
@@ -144,7 +144,7 @@
element._handlePatchChange({detail: {basePatchNum: 1, patchNum: 3}});
assert.equal(navigateToChangeStub.callCount, 1);
assert.isTrue(navigateToChangeStub.lastCall
- .calledWithExactly(element.change, 3, 1));
+ .calledWithExactly(element.change, {patchNum: 3, basePatchNum: 1}));
});
test('class is applied to file list on old patch set', () => {
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 f0d8935..441c233 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
@@ -14,11 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Subscription} from 'rxjs';
import '../../../styles/gr-a11y-styles';
import '../../../styles/shared-styles';
import '../../diff/gr-diff-cursor/gr-diff-cursor';
import '../../diff/gr-diff-host/gr-diff-host';
-import '../../diff/gr-comment-api/gr-comment-api';
import '../../diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog';
import '../../edit/gr-edit-file-controls/gr-edit-file-controls';
import '../../shared/gr-button/gr-button';
@@ -30,7 +30,6 @@
import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
import '../../shared/gr-file-status-chip/gr-file-status-chip';
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-file-list_html';
import {asyncForeach, debounce, DelayedTask} from '../../../utils/async-util';
import {
@@ -43,7 +42,7 @@
import {GerritNav} 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 {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {
DiffViewMode,
ScrollMode,
@@ -72,6 +71,7 @@
FileNameToFileInfoMap,
NumericChangeId,
PatchRange,
+ RevisionPatchSetNum,
} from '../../../types/common';
import {DiffPreferencesInfo} from '../../../types/diff';
import {GrDiffHost} from '../../diff/gr-diff-host/gr-diff-host';
@@ -83,11 +83,11 @@
import {ParsedChangeInfo, PatchSetFile} from '../../../types/types';
import {Timing} from '../../../constants/reporting';
import {RevisionInfo} from '../../shared/revision-info/revision-info';
-import {preferences$} from '../../../services/user/user-model';
-import {changeComments$} from '../../../services/comments/comments-model';
-import {Subject} from 'rxjs';
-import {takeUntil} from 'rxjs/operators';
import {listen} from '../../../services/shortcuts/shortcuts-service';
+import {select} from '../../../utils/observable-util';
+import {resolve, DIPolymerElement} from '../../../models/dependency';
+import {browserModelToken} from '../../../models/browser/browser-model';
+import {commentsModelToken} from '../../../models/comments/comments-model';
export const DEFAULT_NUM_FILES_SHOWN = 200;
@@ -176,7 +176,7 @@
*/
// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
+const base = KeyboardShortcutMixin(DIPolymerElement);
@customElement('gr-file-list')
export class GrFileList extends base {
@@ -221,7 +221,7 @@
_loggedIn = false;
@property({type: Array})
- _reviewed?: string[] = [];
+ reviewed?: string[] = [];
@property({type: Object, notify: true, observer: '_updateDiffPreferences'})
diffPrefs?: DiffPreferencesInfo;
@@ -312,11 +312,19 @@
@property({type: Array})
_dynamicPrependedContentEndpoints?: string[];
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
- disconnected$ = new Subject();
+ private readonly userModel = getAppContext().userModel;
+
+ private readonly changeModel = getAppContext().changeModel;
+
+ private readonly getCommentsModel = resolve(this, commentsModelToken);
+
+ private readonly getBrowserModel = resolve(this, browserModelToken);
+
+ private subscriptions: Subscription[] = [];
/** Called in disconnectedCallback. */
private cleanups: (() => void)[] = [];
@@ -372,11 +380,27 @@
override connectedCallback() {
super.connectedCallback();
- changeComments$
- .pipe(takeUntil(this.disconnected$))
- .subscribe(changeComments => {
+ this.subscriptions = [
+ this.getCommentsModel().changeComments$.subscribe(changeComments => {
this.changeComments = changeComments;
- });
+ }),
+ this.getBrowserModel().diffViewMode$.subscribe(
+ diffView => (this.diffViewMode = diffView)
+ ),
+ this.userModel.diffPreferences$.subscribe(diffPreferences => {
+ this.diffPrefs = diffPreferences;
+ }),
+ select(
+ this.userModel.preferences$,
+ prefs => !!prefs?.size_bar_in_change_table
+ ).subscribe(sizeBarInChangeTable => {
+ this._showSizeBars = sizeBarInChangeTable;
+ }),
+ this.changeModel.reviewedFiles$.subscribe(reviewedFiles => {
+ this.reviewed = reviewedFiles ?? [];
+ }),
+ ];
+
getPluginLoader()
.awaitPluginsLoaded()
.then(() => {
@@ -428,7 +452,10 @@
}
override disconnectedCallback() {
- this.disconnected$.next();
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions = [];
this.diffCursor.dispose();
this.fileCursor.unsetCursor();
this._cancelDiffs();
@@ -448,7 +475,7 @@
this._loading = true;
this.collapseAllDiffs();
- const promises = [];
+ const promises: Promise<boolean | void>[] = [];
promises.push(
this.restApiService
@@ -459,31 +486,9 @@
);
promises.push(
- this._getLoggedIn()
- .then(loggedIn => (this._loggedIn = loggedIn))
- .then(loggedIn => {
- if (!loggedIn) {
- return;
- }
-
- return this._getReviewedFiles(changeNum, patchRange).then(
- reviewed => {
- this._reviewed = reviewed;
- }
- );
- })
+ this._getLoggedIn().then(loggedIn => (this._loggedIn = loggedIn))
);
- promises.push(
- this._getDiffPreferences().then(prefs => {
- this.diffPrefs = prefs;
- })
- );
-
- preferences$.pipe(takeUntil(this.disconnected$)).subscribe(prefs => {
- this._showSizeBars = !!prefs?.size_bar_in_change_table;
- });
-
return Promise.all(promises).then(() => {
this._loading = false;
this._detectChromiteButler();
@@ -743,7 +748,7 @@
throw new Error('changeNum and patchRange must be set');
}
- return this.restApiService.saveFileReviewed(
+ return this.changeModel.setReviewedFilesStatus(
this.changeNum,
this.patchRange.patchNum,
path,
@@ -768,8 +773,7 @@
const paths = Object.keys(response).sort(specialFilePathCompare);
const files: NormalizedFileInfo[] = [];
for (let i = 0; i < paths.length; i++) {
- // TODO(TS): make copy instead of as NormalizedFileInfo
- const info = response[paths[i]] as NormalizedFileInfo;
+ const info = {...response[paths[i]]} as NormalizedFileInfo;
info.__path = paths[i];
info.lines_inserted = info.lines_inserted || 0;
info.lines_deleted = info.lines_deleted || 0;
@@ -1017,28 +1021,23 @@
_computeDiffURL(
change?: ParsedChangeInfo,
- patchRange?: PatchRange,
+ basePatchNum?: BasePatchSetNum,
+ patchNum?: RevisionPatchSetNum,
path?: string,
editMode?: boolean
) {
- // Polymer 2: check for undefined
if (
change === undefined ||
- !patchRange?.patchNum ||
+ patchNum === undefined ||
path === undefined ||
editMode === undefined
) {
return;
}
if (editMode && path !== SpecialFilePath.MERGE_LIST) {
- return GerritNav.getEditUrlForDiff(change, path, patchRange.patchNum);
+ return GerritNav.getEditUrlForDiff(change, path, patchNum);
}
- return GerritNav.getUrlForDiff(
- change,
- path,
- patchRange.patchNum,
- patchRange.basePatchNum
- );
+ return GerritNav.getUrlForDiff(change, path, patchNum, basePatchNum);
}
_formatBytes(bytes?: number) {
@@ -1116,18 +1115,17 @@
_handleShowParent1(): void {
if (!this.change || !this.patchRange) return;
- GerritNav.navigateToChange(
- this.change,
- this.patchRange.patchNum,
- -1 as BasePatchSetNum // Parent 1
- );
+ GerritNav.navigateToChange(this.change, {
+ patchNum: this.patchRange.patchNum,
+ basePatchNum: -1 as BasePatchSetNum, // Parent 1
+ });
}
@observe(
'_filesByPath',
'changeComments',
'patchRange',
- '_reviewed',
+ 'reviewed',
'_loading'
)
_computeFiles(
@@ -1355,8 +1353,6 @@
diffElements: GrDiffHost[],
initialCount: number
) {
- let iter = 0;
-
for (const file of files) {
const path = file.path;
const diffElem = this._findDiffByPath(path, diffElements);
@@ -1369,8 +1365,6 @@
const path = file.path;
this._cancelForEachDiff = cancel;
- iter++;
- console.info('Expanding diff', iter, 'of', initialCount, ':', path);
const diffElem = this._findDiffByPath(path, diffElements);
if (!diffElem) {
this.reporting.error(
@@ -1389,7 +1383,6 @@
return Promise.all(promises);
}).then(() => {
this._cancelForEachDiff = undefined;
- console.info('Finished expanding', initialCount, 'diff(s)');
this.reporting.timeEndWithAverage(
Timing.FILE_EXPAND_ALL,
Timing.FILE_EXPAND_ALL_AVG,
@@ -1564,7 +1557,7 @@
} else if (!this._showBarsForPath(path)) {
hideClass = 'invisible';
}
- return `sizeBars desktop ${hideClass}`;
+ return `sizeBars ${hideClass}`;
}
/**
@@ -1640,9 +1633,7 @@
}
_handleReloadingDiffPreference() {
- this._getDiffPreferences().then(prefs => {
- this.diffPrefs = prefs;
- });
+ this.userModel.getDiffPreferences();
}
/**
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
index f7be36b..67ca9be 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
@@ -37,13 +37,15 @@
max-width: 1px;
overflow: hidden;
display: none;
+ vertical-align: top;
}
div[role='gridcell']
> div.comments
> span:empty
+ span:empty
+ span.noCommentsScreenReaderText {
- display: inline;
+ /* inline-block instead of block, such that it can control width */
+ display: inline-block;
}
:host(.loading) .row {
opacity: 0.5;
@@ -126,6 +128,7 @@
.comments {
padding-left: var(--spacing-l);
min-width: 7.5em;
+ white-space: nowrap;
}
.row:not(.header-row) .stats,
.total-stats {
@@ -263,8 +266,19 @@
visibility: visible;
}
- /** small screen breakpoint: 768px */
- @media screen and (max-width: 55em) {
+ @media screen and (max-width: 1200px) {
+ gr-endpoint-decorator.extra-col {
+ display: none;
+ }
+ }
+
+ @media screen and (max-width: 1000px) {
+ .reviewed {
+ display: none;
+ }
+ }
+
+ @media screen and (max-width: 800px) {
.desktop {
display: none;
}
@@ -281,9 +295,6 @@
.status {
justify-content: flex-start;
}
- .reviewed {
- display: none;
- }
.comments {
min-width: initial;
}
@@ -315,7 +326,11 @@
items="[[_dynamicPrependedHeaderEndpoints]]"
as="headerEndpoint"
>
- <gr-endpoint-decorator name$="[[headerEndpoint]]" role="columnheader">
+ <gr-endpoint-decorator
+ class="prepended-col"
+ name$="[[headerEndpoint]]"
+ role="columnheader"
+ >
<gr-endpoint-param name="change" value="[[change]]">
</gr-endpoint-param>
<gr-endpoint-param name="patchRange" value="[[patchRange]]">
@@ -326,8 +341,9 @@
</template>
</template>
<div class="path" role="columnheader">File</div>
- <div class="comments" role="columnheader">Comments</div>
- <div class="sizeBars" role="columnheader">Size</div>
+ <div class="comments desktop" role="columnheader">Comments</div>
+ <div class="comments mobile" role="columnheader" title="Comments">C</div>
+ <div class="sizeBars desktop" role="columnheader">Size</div>
<div class="header-stats" role="columnheader">Delta</div>
<!-- endpoint: change-view-file-list-header -->
<template is="dom-if" if="[[_showDynamicColumns]]">
@@ -336,7 +352,11 @@
items="[[_dynamicHeaderEndpoints]]"
as="headerEndpoint"
>
- <gr-endpoint-decorator name$="[[headerEndpoint]]" role="columnheader">
+ <gr-endpoint-decorator
+ class="extra-col"
+ name$="[[headerEndpoint]]"
+ role="columnheader"
+ >
</gr-endpoint-decorator>
</template>
</template>
@@ -373,7 +393,11 @@
items="[[_dynamicPrependedContentEndpoints]]"
as="contentEndpoint"
>
- <gr-endpoint-decorator name="[[contentEndpoint]]" role="gridcell">
+ <gr-endpoint-decorator
+ class="prepended-col"
+ name="[[contentEndpoint]]"
+ role="gridcell"
+ >
<gr-endpoint-param name="change" value="[[change]]">
</gr-endpoint-param>
<gr-endpoint-param name="changeNum" value="[[changeNum]]">
@@ -389,13 +413,13 @@
</template>
<!-- TODO: Remove data-url as it appears its not used -->
<span
- data-url="[[_computeDiffURL(change, patchRange, file.__path, editMode)]]"
+ data-url="[[_computeDiffURL(change, patchRange.basePatchNum, patchRange.patchNum, file.__path, editMode)]]"
class="path"
role="gridcell"
>
<a
class="pathLink"
- href$="[[_computeDiffURL(change, patchRange, file.__path, editMode)]]"
+ href$="[[_computeDiffURL(change, patchRange.basePatchNum, patchRange.patchNum, file.__path, editMode)]]"
>
<span
title$="[[_computeDisplayPath(file.__path)]]"
@@ -472,7 +496,7 @@
</span>
</div>
</div>
- <div role="gridcell">
+ <div class="desktop" role="gridcell">
<!-- The content must be in a separate div. It guarantees, that
gridcell always visible for screen readers.
For example, without a nested div screen readers pronounce the
@@ -540,7 +564,10 @@
as="contentEndpoint"
>
<div class$="[[_computeClass('', file.__path)]]" role="gridcell">
- <gr-endpoint-decorator name="[[contentEndpoint]]">
+ <gr-endpoint-decorator
+ class="extra-col"
+ name="[[contentEndpoint]]"
+ >
<gr-endpoint-param name="change" value="[[change]]">
</gr-endpoint-param>
<gr-endpoint-param name="changeNum" value="[[changeNum]]">
@@ -643,7 +670,6 @@
prefs="[[diffPrefs]]"
project-name="[[change.project]]"
no-render-on-prefs-change=""
- view-mode="[[diffViewMode]]"
></gr-diff-host>
</template>
</div>
@@ -660,7 +686,11 @@
items="[[_dynamicPrependedContentEndpoints]]"
as="contentEndpoint"
>
- <gr-endpoint-decorator name="[[contentEndpoint]]" role="gridcell">
+ <gr-endpoint-decorator
+ class="prepended-col"
+ name="[[contentEndpoint]]"
+ role="gridcell"
+ >
<gr-endpoint-param name="change" value="[[change]]">
</gr-endpoint-param>
<gr-endpoint-param name="changeNum" value="[[changeNum]]">
@@ -723,7 +753,7 @@
items="[[_dynamicSummaryEndpoints]]"
as="summaryEndpoint"
>
- <gr-endpoint-decorator name="[[summaryEndpoint]]">
+ <gr-endpoint-decorator class="extra-col" name="[[summaryEndpoint]]">
<gr-endpoint-param
name="change"
value="[[change]]"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
index 7409be7..bd12eae 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
@@ -16,7 +16,6 @@
*/
import '../../../test/common-test-setup-karma.js';
-import '../../diff/gr-comment-api/gr-comment-api.js';
import '../../shared/gr-date-formatter/gr-date-formatter.js';
import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
import './gr-file-list.js';
@@ -47,9 +46,7 @@
const commentApiMock = createCommentApiMockWithTemplateElement(
'gr-file-list-comment-api-mock', html`
- <gr-file-list id="fileList"
- change-comments="[[_changeComments]]"></gr-file-list>
- <gr-comment-api id="commentAPI"></gr-comment-api>
+ <gr-file-list id="fileList"></gr-file-list>
`);
const basicFixture = fixtureFromElement(commentApiMock.is);
@@ -665,7 +662,7 @@
});
test('file review status', () => {
- element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
+ element.reviewed = ['/COMMIT_MSG', 'myfile.txt'];
element._filesByPath = {
'/COMMIT_MSG': {},
'file_added_in_rev2.txt': {},
@@ -820,10 +817,8 @@
MockInteractions.tap(row);
flush();
- const diffDisplay = element.diffs[0];
- element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'};
+ element._userPrefs = {diff_view: 'SIDE_BY_SIDE'};
element.set('diffViewMode', 'UNIFIED_DIFF');
- assert.equal(diffDisplay.viewMode, 'UNIFIED_DIFF');
assert.isTrue(element._updateDiffPreferences.called);
});
@@ -1227,11 +1222,8 @@
project: 'gerrit',
};
const path = 'index.php';
- const patchRange = {
- patchNum: 1,
- };
assert.equal(
- element._computeDiffURL(change, patchRange, path, false),
+ element._computeDiffURL(change, undefined, 1, path, false),
'/c/gerrit/+/1/1/index.php');
diffStub.restore();
});
@@ -1244,11 +1236,8 @@
project: 'gerrit',
};
const path = '/COMMIT_MSG';
- const patchRange = {
- patchNum: 1,
- };
assert.equal(
- element._computeDiffURL(change, patchRange, path, false),
+ element._computeDiffURL(change, undefined, 1, path, false),
'/c/gerrit/+/1/1//COMMIT_MSG');
diffStub.restore();
});
@@ -1261,11 +1250,8 @@
project: 'gerrit',
};
const path = 'index.php';
- const patchRange = {
- patchNum: 1,
- };
assert.equal(
- element._computeDiffURL(change, patchRange, path, true),
+ element._computeDiffURL(change, undefined, 1, path, true),
'/c/gerrit/+/1/edit/index.php,edit');
editStub.restore();
});
@@ -1278,11 +1264,8 @@
project: 'gerrit',
};
const path = '/COMMIT_MSG';
- const patchRange = {
- patchNum: 1,
- };
assert.equal(
- element._computeDiffURL(change, patchRange, path, true),
+ element._computeDiffURL(change, undefined, 1, path, true),
'/c/gerrit/+/1/edit//COMMIT_MSG,edit');
editStub.restore();
});
@@ -1414,11 +1397,11 @@
test('_computeSizeBarsClass', () => {
assert.equal(element._computeSizeBarsClass(false, 'foo/bar.baz'),
- 'sizeBars desktop hide');
+ 'sizeBars hide');
assert.equal(element._computeSizeBarsClass(true, '/COMMIT_MSG'),
- 'sizeBars desktop invisible');
+ 'sizeBars invisible');
assert.equal(element._computeSizeBarsClass(true, 'foo/bar.baz'),
- 'sizeBars desktop ');
+ 'sizeBars ');
});
});
@@ -1474,18 +1457,11 @@
ignore_whitespace: 'IGNORE_NONE',
};
diff.diff = getMockDiffResponse();
- sinon.stub(diff.changeComments, 'getCommentsForPath')
- .withArgs('/COMMIT_MSG', {
- basePatchNum: 'PARENT',
- patchNum: 2,
- })
- .returns(diff.comments);
await listenOnce(diff, 'render');
}
async function renderAndGetNewDiffs(index) {
- const diffs =
- element.root.querySelectorAll('gr-diff-host');
+ const diffs = element.root.querySelectorAll('gr-diff-host');
for (let i = index; i < diffs.length; i++) {
await setupDiff(diffs[i]);
@@ -1533,7 +1509,7 @@
size: 100,
},
};
- element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
+ element.reviewed = ['/COMMIT_MSG', 'myfile.txt'];
element._loggedIn = true;
element.changeNum = '42';
element.patchRange = {
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts
index d50e00f..8d515da 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.ts
@@ -22,7 +22,7 @@
import {htmlTemplate} from './gr-included-in-dialog_html';
import {customElement, property} from '@polymer/decorators';
import {IncludedInInfo, NumericChangeId} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
interface DisplayGroup {
title: string;
@@ -53,7 +53,7 @@
@property({type: String})
_filterText = '';
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
loadData() {
if (!this.changeNum) {
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
index a496be5..be04e6e 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.ts
@@ -32,10 +32,15 @@
Label,
LabelValuesMap,
} from '../gr-label-score-row/gr-label-score-row';
-import {appContext} from '../../../services/app-context';
-import {labelCompare} from '../../../utils/label-util';
+import {getAppContext} from '../../../services/app-context';
+import {
+ getTriggerVotes,
+ labelCompare,
+ showNewSubmitRequirements,
+} from '../../../utils/label-util';
import {Execution} from '../../../constants/reporting';
import {ChangeStatus} from '../../../constants/constants';
+import {fontStyles} from '../../../styles/gr-font-styles';
@customElement('gr-label-scores')
export class GrLabelScores extends LitElement {
@@ -48,10 +53,13 @@
@property({type: Object})
account?: AccountInfo;
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
+
+ private readonly flagsService = getAppContext().flagsService;
static override get styles() {
return [
+ fontStyles,
css`
.scoresTable {
display: table;
@@ -72,26 +80,74 @@
gr-label-score-row.no-access {
display: none;
}
+ .heading-3 {
+ padding-left: var(--spacing-xl);
+ margin-bottom: var(--spacing-m);
+ margin-top: var(--spacing-l);
+ }
+ .heading-3:first-of-type {
+ margin-top: 0;
+ }
`,
];
}
override render() {
+ if (showNewSubmitRequirements(this.flagsService, this.change)) {
+ return this.renderNewSubmitRequirements();
+ } else {
+ return this.renderOldSubmitRequirements();
+ }
+ }
+
+ private renderOldSubmitRequirements() {
const labels = this._computeLabels();
+ return html`${this.renderLabels(labels)}${this.renderErrorMessages()}`;
+ }
+
+ private renderNewSubmitRequirements() {
+ return html`${this.renderSubmitReqsLabels()}${this.renderTriggerVotes()}
+ ${this.renderErrorMessages()}`;
+ }
+
+ private renderSubmitReqsLabels() {
+ const triggerVotes = getTriggerVotes(this.change);
+ const labels = this._computeLabels().filter(
+ label => !triggerVotes.includes(label.name)
+ );
+ if (!labels.length) return;
+ return html`<h3 class="heading-3">Submit requirements votes</h3>
+ ${this.renderLabels(labels)}`;
+ }
+
+ private renderTriggerVotes() {
+ const triggerVotes = getTriggerVotes(this.change);
+ const labels = this._computeLabels().filter(label =>
+ triggerVotes.includes(label.name)
+ );
+ if (!labels.length) return;
+ return html`<h3 class="heading-3">Trigger Votes</h3>
+ ${this.renderLabels(labels)}`;
+ }
+
+ private renderLabels(labels: Label[]) {
const labelValues = this._computeColumns();
return html`<div class="scoresTable">
- ${labels.map(
- label => html`<gr-label-score-row
- class="${this.computeLabelAccessClass(label.name)}"
- .label="${label}"
- .name="${label.name}"
- .labels="${this.change?.labels}"
- .permittedLabels="${this.permittedLabels}"
- .labelValues="${labelValues}"
- ></gr-label-score-row>`
- )}
- </div>
- <div
+ ${labels.map(
+ label => html`<gr-label-score-row
+ class="${this.computeLabelAccessClass(label.name)}"
+ .label="${label}"
+ .name="${label.name}"
+ .labels="${this.change?.labels}"
+ .permittedLabels="${this.permittedLabels}"
+ .labelValues="${labelValues}"
+ ></gr-label-score-row>`
+ )}
+ </div>`;
+ }
+
+ private renderErrorMessages() {
+ return html`<div
class="mergedMessage"
?hidden=${this.change?.status !== ChangeStatus.MERGED}
>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
index 95e4301..cc7cf88 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -42,7 +42,7 @@
} from '../../../types/common';
import {CommentThread} from '../../../utils/comment-util';
import {hasOwnProperty} from '../../../utils/common-util';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {pluralize} from '../../../utils/string-util';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {
@@ -53,7 +53,7 @@
import {isServiceUser, replaceTemplates} from '../../../utils/account-util';
const PATCH_SET_PREFIX_PATTERN = /^(?:Uploaded\s*)?[Pp]atch [Ss]et \d+:\s*(.*)/;
-const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?[.]?$/;
+const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?[.:]?$/;
const UPLOADED_NEW_PATCHSET_PATTERN = /Uploaded patch set (\d+)./;
const MERGED_PATCHSET_PATTERN = /(\d+) is the latest approved patch-set/;
const VOTE_RESET_TEXT = '0 (vote reset)';
@@ -202,7 +202,7 @@
})
_commentCountText = '';
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
@@ -333,7 +333,7 @@
patchNum = computeLatestPatchNum(computeAllPatchSets(this.change))!;
basePatchNum = computePredecessor(patchNum)!;
}
- GerritNav.navigateToChange(this.change, patchNum, basePatchNum);
+ GerritNav.navigateToChange(this.change, {patchNum, basePatchNum});
// stop propagation to stop message expansion
e.stopPropagation();
}
@@ -554,7 +554,11 @@
}
@observe('projectName')
- _projectNameChanged(name: string) {
+ _projectNameChanged(name?: string) {
+ if (!name) {
+ this._projectConfig = undefined;
+ return;
+ }
this.restApiService.getProjectConfig(name as RepoName).then(config => {
this._projectConfig = config;
});
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
index 7f3e9de..8def279 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.ts
@@ -280,13 +280,11 @@
</div>
</template>
<gr-thread-list
- change="[[change]]"
hidden$="[[!commentThreads.length]]"
threads="[[commentThreads]]"
- change-num="[[changeNum]]"
- logged-in="[[_loggedIn]]"
hide-dropdown
show-comment-context
+ message-id="[[message.id]]"
>
</gr-thread-list>
</template>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
index f87c4c3..0fd39d2 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.ts
@@ -239,19 +239,6 @@
assert.isNotOk(element._computeShowOnBehalfOf(message));
});
- ['Trybot-Ready', 'Tryjob-Request', 'Commit-Queue'].forEach(label => {
- test(`${label} ignored for color voting`, () => {
- element.message = {
- ...createChangeMessage(),
- author: {},
- expanded: false,
- message: `Patch Set 1: ${label}+1`,
- };
- assert.isNotOk(query(element, '.negativeVote'));
- assert.isNotOk(query(element, '.positiveVote'));
- });
- });
-
test('clicking on date link fires event', () => {
element.message = {
...createChangeMessage(),
@@ -285,11 +272,10 @@
};
element._handleViewPatchsetDiff(new MouseEvent('click'));
assert.isTrue(
- navStub.calledWithExactly(
- element.change!,
- 1 as PatchSetNum,
- 'PARENT' as BasePatchSetNum
- )
+ navStub.calledWithExactly(element.change!, {
+ patchNum: 1 as PatchSetNum,
+ basePatchNum: 'PARENT' as BasePatchSetNum,
+ })
);
});
@@ -300,11 +286,10 @@
};
element._handleViewPatchsetDiff(new MouseEvent('click'));
assert.isTrue(
- navStub.calledWithExactly(
- element.change!,
- 2 as PatchSetNum,
- 1 as BasePatchSetNum
- )
+ navStub.calledWithExactly(element.change!, {
+ patchNum: 2 as PatchSetNum,
+ basePatchNum: 1 as BasePatchSetNum,
+ })
);
element.message = {
@@ -313,11 +298,10 @@
};
element._handleViewPatchsetDiff(new MouseEvent('click'));
assert.isTrue(
- navStub.calledWithExactly(
- element.change!,
- 200 as PatchSetNum,
- 199 as BasePatchSetNum
- )
+ navStub.calledWithExactly(element.change!, {
+ patchNum: 200 as PatchSetNum,
+ basePatchNum: 199 as BasePatchSetNum,
+ })
);
});
@@ -328,11 +312,10 @@
};
element._handleViewPatchsetDiff(new MouseEvent('click'));
assert.isTrue(
- navStub.calledWithExactly(
- element.change!,
- 4 as PatchSetNum,
- 3 as BasePatchSetNum
- )
+ navStub.calledWithExactly(element.change!, {
+ patchNum: 4 as PatchSetNum,
+ basePatchNum: 3 as BasePatchSetNum,
+ })
);
});
@@ -343,11 +326,10 @@
};
element._handleViewPatchsetDiff(new MouseEvent('click'));
assert.isTrue(
- navStub.calledWithExactly(
- element.change!,
- 4 as PatchSetNum,
- 3 as BasePatchSetNum
- )
+ navStub.calledWithExactly(element.change!, {
+ patchNum: 4 as PatchSetNum,
+ basePatchNum: 3 as BasePatchSetNum,
+ })
);
});
});
@@ -536,6 +518,23 @@
assert.isFalse(scoreChips[2].classList.contains('min'));
});
+ test('Uploaded and rebased', () => {
+ element.message = {
+ ...createChangeMessage(),
+ author: {},
+ expanded: false,
+ message:
+ 'Uploaded patch set 4: Commit-Queue+1: Patch Set 3 was rebased.',
+ };
+ element.labelExtremes = {
+ 'Commit-Queue': {max: 2, min: -2},
+ };
+ flush();
+ const scoreChips = queryAll(element, '.score');
+ assert.equal(scoreChips.length, 1);
+ assert.isTrue(scoreChips[0].classList.contains('positive'));
+ });
+
test('removed votes', () => {
element.message = {
...createChangeMessage(),
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 e16c073..6b1ecf2 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
@@ -14,12 +14,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Subscription} from 'rxjs';
import '@polymer/paper-toggle-button/paper-toggle-button';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-icons/gr-icons';
import '../gr-message/gr-message';
+import '../../../styles/gr-paper-styles';
import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-messages-list_html';
import {
Shortcut,
@@ -27,13 +28,12 @@
} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {parseDate} from '../../../utils/date-util';
import {MessageTag} from '../../../constants/constants';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {customElement, property} from '@polymer/decorators';
import {
ChangeId,
ChangeMessageId,
ChangeMessageInfo,
- ChangeViewChangeInfo,
LabelNameToInfoMap,
NumericChangeId,
PatchSetNum,
@@ -41,13 +41,17 @@
ReviewerUpdateInfo,
VotingRangeInfo,
} from '../../../types/common';
-import {ChangeComments} from '../../diff/gr-comment-api/gr-comment-api';
import {CommentThread, isRobot} from '../../../utils/comment-util';
import {GrMessage, MessageAnchorTapDetail} from '../gr-message/gr-message';
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
import {DomRepeat} from '@polymer/polymer/lib/elements/dom-repeat';
import {getVotingRange} from '../../../utils/label-util';
-import {FormattedReviewerUpdateInfo} from '../../../types/types';
+import {
+ FormattedReviewerUpdateInfo,
+ ParsedChangeInfo,
+} from '../../../types/types';
+import {commentsModelToken} from '../../../models/comments/comments-model';
+import {resolve, DIPolymerElement} from '../../../models/dependency';
/**
* The content of the enum is also used in the UI for the button text.
@@ -91,17 +95,10 @@
message: CombinedMessage,
allThreadsForChange: CommentThread[]
): CommentThread[] {
- if (message._index === undefined) {
- return [];
- }
+ if (message._index === undefined) return [];
const messageId = getMessageId(message);
return allThreadsForChange.filter(thread =>
- thread.comments.some(comment => {
- const matchesMessage = comment.change_message_id === messageId;
- if (!matchesMessage) return false;
- comment.collapsed = !matchesMessage;
- return matchesMessage;
- })
+ thread.comments.some(comment => comment.change_message_id === messageId)
);
}
@@ -133,9 +130,6 @@
if (message.tag === MessageTag.TAG_NEW_WIP_PATCHSET) {
return MessageTag.TAG_NEW_PATCHSET;
}
- if (message.tag === MessageTag.TAG_UNSET_ASSIGNEE) {
- return MessageTag.TAG_SET_ASSIGNEE;
- }
if (message.tag === MessageTag.TAG_UNSET_PRIVATE) {
return MessageTag.TAG_SET_PRIVATE;
}
@@ -201,14 +195,16 @@
}
@customElement('gr-messages-list')
-export class GrMessagesList extends PolymerElement {
+export class GrMessagesList extends DIPolymerElement {
static get template() {
return htmlTemplate;
}
+ // Private internal @state, derived from the application state.
@property({type: Object})
- change?: ChangeViewChangeInfo;
+ change?: ParsedChangeInfo;
+ // Private internal @state, derived from the application state.
@property({type: String})
changeNum?: ChangeId | NumericChangeId;
@@ -218,12 +214,15 @@
@property({type: Array})
reviewerUpdates: ReviewerUpdateInfo[] = [];
+ // Private internal @state, derived from the application state.
@property({type: Object})
- changeComments?: ChangeComments;
+ commentThreads: CommentThread[] = [];
+ // Private internal @state, derived from the application state.
@property({type: String})
projectName?: RepoName;
+ // Private internal @state, derived from the application state.
@property({type: Boolean})
showReplyButtons = false;
@@ -243,7 +242,7 @@
type: Array,
computed:
'_computeCombinedMessages(messages, reviewerUpdates, ' +
- 'changeComments)',
+ 'commentThreads)',
observer: '_combinedMessagesChanged',
})
_combinedMessages: CombinedMessage[] = [];
@@ -251,9 +250,55 @@
@property({type: Object, computed: '_computeLabelExtremes(labels.*)'})
_labelExtremes: {[labelName: string]: VotingRangeInfo} = {};
- private readonly reporting = appContext.reportingService;
+ private readonly userModel = getAppContext().userModel;
- private readonly shortcuts = appContext.shortcutsService;
+ // Private but used in tests.
+ readonly getCommentsModel = resolve(this, commentsModelToken);
+
+ private readonly changeModel = getAppContext().changeModel;
+
+ private readonly reporting = getAppContext().reportingService;
+
+ private readonly shortcuts = getAppContext().shortcutsService;
+
+ private subscriptions: Subscription[] = [];
+
+ override connectedCallback() {
+ super.connectedCallback();
+ this.subscriptions.push(
+ this.getCommentsModel().threads$.subscribe(x => {
+ this.commentThreads = x;
+ })
+ );
+ this.subscriptions.push(
+ this.changeModel.change$.subscribe(x => {
+ this.change = x;
+ })
+ );
+ this.subscriptions.push(
+ this.userModel.loggedIn$.subscribe(x => {
+ this.showReplyButtons = x;
+ })
+ );
+ this.subscriptions.push(
+ this.changeModel.repo$.subscribe(x => {
+ this.projectName = x;
+ })
+ );
+ this.subscriptions.push(
+ this.changeModel.changeNum$.subscribe(x => {
+ this.changeNum = x;
+ })
+ );
+ }
+
+ override disconnectedCallback() {
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions = [];
+ super.disconnectedCallback();
+ }
scrollToMessage(messageID: string) {
const selector = `[data-message-id="${messageID}"]`;
@@ -304,16 +349,11 @@
* all messages and updates, aligns or massages some of the properties.
*/
_computeCombinedMessages(
- messages?: ChangeMessageInfo[],
- reviewerUpdates?: FormattedReviewerUpdateInfo[],
- changeComments?: ChangeComments
+ messages: ChangeMessageInfo[] | undefined,
+ reviewerUpdates: FormattedReviewerUpdateInfo[] | undefined,
+ commentThreads: CommentThread[]
) {
- if (
- messages === undefined ||
- reviewerUpdates === undefined ||
- changeComments === undefined
- )
- return [];
+ if (messages === undefined || reviewerUpdates === undefined) return;
let mi = 0;
let ri = 0;
@@ -345,20 +385,12 @@
}
}
- const allThreadsForChange = changeComments.getAllThreadsForChange();
- // collapse all by default
- for (const thread of allThreadsForChange) {
- for (const comment of thread.comments) {
- comment.collapsed = true;
- }
- }
-
for (let i = 0; i < combinedMessages.length; i++) {
const message = combinedMessages[i];
if (message.expanded === undefined) {
message.expanded = false;
}
- message.commentThreads = computeThreads(message, allThreadsForChange);
+ message.commentThreads = computeThreads(message, commentThreads);
message._revision_number = computeRevision(message, combinedMessages);
message.tag = computeTag(message);
}
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts
index 56fae87..087ee19 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts
@@ -51,6 +51,9 @@
border-bottom: 1px solid var(--border-color);
}
</style>
+ <style include="gr-paper-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<div class="header">
<div id="showAllActivityToggleContainer" class="container">
<template
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.js
index 0939daa..0a69d8a 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.js
@@ -16,21 +16,16 @@
*/
import '../../../test/common-test-setup-karma.js';
-import '../../diff/gr-comment-api/gr-comment-api.js';
import './gr-messages-list.js';
import {createCommentApiMockWithTemplateElement} from '../../../test/mocks/comment-api.js';
import {TEST_ONLY} from './gr-messages-list.js';
import {MessageTag} from '../../../constants/constants.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
import {stubRestApi} from '../../../test/test-utils.js';
-import {ChangeComments} from '../../diff/gr-comment-api/gr-comment-api.js';
createCommentApiMockWithTemplateElement(
'gr-messages-list-comment-mock-api', html`
- <gr-messages-list
- id="messagesList"
- change-comments="[[_changeComments]]"></gr-messages-list>
- <gr-comment-api id="commentAPI"></gr-comment-api>
+ <gr-messages-list id="messagesList"></gr-messages-list>
`);
const basicFixture = fixtureFromTemplate(html`
@@ -133,7 +128,7 @@
};
suite('basic tests', () => {
- setup(() => {
+ setup(async () => {
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
stubRestApi('getDiffComments').returns(Promise.resolve(comments));
stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
@@ -144,9 +139,9 @@
// comment API.
commentApiWrapper = basicFixture.instantiate();
element = commentApiWrapper.$.messagesList;
- element.changeComments = new ChangeComments(comments);
+ await element.getCommentsModel().reloadComments();
element.messages = messages;
- flush();
+ await flush();
});
test('expand/collapse all', () => {
@@ -453,7 +448,6 @@
// comment API.
commentApiWrapper = basicFixture.instantiate();
element = commentApiWrapper.$.messagesList;
- element.changeComments = new ChangeComments();
element.messages = messages;
flush();
});
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
index e4703df..744db3b 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
@@ -35,6 +35,9 @@
href?: string;
@property()
+ label?: string;
+
+ @property()
showSubmittableCheck = false;
@property()
@@ -110,7 +113,12 @@
const linkClass = this._computeLinkClass(change);
return html`
<div class="changeContainer">
- <a href="${ifDefined(this.href)}" class="${linkClass}"><slot></slot></a>
+ <a
+ href="${ifDefined(this.href)}"
+ aria-label="${ifDefined(this.label)}"
+ class="${linkClass}"
+ ><slot></slot
+ ></a>
${this.showSubmittableCheck
? html`<span
tabindex="-1"
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 7493e2f..c882764 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
@@ -30,9 +30,10 @@
PatchSetNum,
CommitId,
} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {ParsedChangeInfo} from '../../../types/types';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {truncatePath} from '../../../utils/path-list-util';
import {pluralize} from '../../../utils/string-util';
import {
changeIsOpen,
@@ -89,7 +90,7 @@
@state()
sameTopicChanges: ChangeInfo[] = [];
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
static override get styles() {
return [
@@ -146,6 +147,45 @@
this.conflictingChanges.length,
this.cherryPickChanges.length
);
+
+ const sectionRenderers = [
+ this.renderRelationChain,
+ this.renderSubmittedTogether,
+ this.renderSameTopic,
+ this.renderMergeConflicts,
+ this.renderCherryPicks,
+ ];
+
+ let firstNonEmptySectionFound = false;
+ const sections = [];
+ for (const renderer of sectionRenderers) {
+ const section: TemplateResult<1> | undefined = renderer.call(
+ this,
+ !firstNonEmptySectionFound,
+ sectionSize
+ );
+ firstNonEmptySectionFound = firstNonEmptySectionFound || !!section;
+ sections.push(section);
+ }
+
+ return html`<gr-endpoint-decorator name="related-changes-section">
+ <gr-endpoint-param
+ name="change"
+ .value=${this.change}
+ ></gr-endpoint-param>
+ <gr-endpoint-slot name="top"></gr-endpoint-slot>
+ ${sections}
+ <gr-endpoint-slot name="bottom"></gr-endpoint-slot>
+ </gr-endpoint-decorator>`;
+ }
+
+ private renderRelationChain(
+ isFirst: boolean,
+ sectionSize: (section: Section) => number
+ ) {
+ if (this.relatedChanges.length === 0) {
+ return undefined;
+ }
const relatedChangesMarkersPredicate = this.markersPredicateFactory(
this.relatedChanges.length,
this.relatedChanges.findIndex(relatedChange =>
@@ -158,17 +198,11 @@
this.patchNum,
this.relatedChanges
);
- let firstNonEmptySectionFound = false;
- let isFirstNonEmpty =
- !firstNonEmptySectionFound && !!this.relatedChanges.length;
- firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
- const relatedChangeSection = html` <section
- id="relatedChanges"
- ?hidden=${!this.relatedChanges.length}
- >
+
+ return html`<section id="relatedChanges">
<gr-related-collapse
title="Relation chain"
- class="${classMap({first: isFirstNonEmpty})}"
+ class="${classMap({first: isFirst})}"
.length=${this.relatedChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.RELATED_CHANGES)}
>
@@ -200,8 +234,19 @@
)}
</gr-related-collapse>
</section>`;
+ }
+ private renderSubmittedTogether(
+ isFirst: boolean,
+ sectionSize: (section: Section) => number
+ ) {
const submittedTogetherChanges = this.submittedTogether?.changes ?? [];
+ if (
+ !submittedTogetherChanges.length &&
+ !this.submittedTogether?.non_visible_changes
+ ) {
+ return undefined;
+ }
const countNonVisibleChanges =
this.submittedTogether?.non_visible_changes ?? 0;
const submittedTogetherMarkersPredicate = this.markersPredicateFactory(
@@ -211,19 +256,10 @@
),
sectionSize(Section.SUBMITTED_TOGETHER)
);
- isFirstNonEmpty =
- !firstNonEmptySectionFound &&
- (!!submittedTogetherChanges?.length ||
- !!this.submittedTogether?.non_visible_changes);
- firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
- const submittedTogetherSection = html`<section
- id="submittedTogether"
- ?hidden=${!submittedTogetherChanges?.length &&
- !this.submittedTogether?.non_visible_changes}
- >
+ return html`<section id="submittedTogether">
<gr-related-collapse
title="Submitted together"
- class="${classMap({first: isFirstNonEmpty})}"
+ class="${classMap({first: isFirst})}"
.length=${submittedTogetherChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.SUBMITTED_TOGETHER)}
>
@@ -239,14 +275,14 @@
${this.renderMarkers(
submittedTogetherMarkersPredicate(index)
)}<gr-related-change
+ .label="${this.renderChangeTitle(change)}"
.change="${change}"
.href="${GerritNav.getUrlForChangeById(
change._number,
change.project
)}"
.showSubmittableCheck=${true}
- >${change.project}: ${change.branch}:
- ${change.subject}</gr-related-change
+ >${this.renderChangeLine(change)}</gr-related-change
>
</div>`
)}
@@ -255,22 +291,25 @@
(+ ${pluralize(countNonVisibleChanges, 'non-visible change')})
</div>
</section>`;
+ }
+
+ private renderSameTopic(
+ isFirst: boolean,
+ sectionSize: (section: Section) => number
+ ) {
+ if (!this.sameTopicChanges?.length) {
+ return undefined;
+ }
const sameTopicMarkersPredicate = this.markersPredicateFactory(
this.sameTopicChanges.length,
-1,
sectionSize(Section.SAME_TOPIC)
);
- isFirstNonEmpty =
- !firstNonEmptySectionFound && !!this.sameTopicChanges?.length;
- firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
- const sameTopicSection = html`<section
- id="sameTopic"
- ?hidden=${!this.sameTopicChanges?.length}
- >
+ return html`<section id="sameTopic">
<gr-related-collapse
title="Same topic"
- class="${classMap({first: isFirstNonEmpty})}"
+ class="${classMap({first: isFirst})}"
.length=${this.sameTopicChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.SAME_TOPIC)}
>
@@ -287,33 +326,35 @@
sameTopicMarkersPredicate(index)
)}<gr-related-change
.change="${change}"
+ .label="${this.renderChangeTitle(change)}"
.href="${GerritNav.getUrlForChangeById(
change._number,
change.project
)}"
- >${change.project}: ${change.branch}:
- ${change.subject}</gr-related-change
+ >${this.renderChangeLine(change)}</gr-related-change
>
</div>`
)}
</gr-related-collapse>
</section>`;
+ }
+ private renderMergeConflicts(
+ isFirst: boolean,
+ sectionSize: (section: Section) => number
+ ) {
+ if (!this.conflictingChanges?.length) {
+ return undefined;
+ }
const mergeConflictsMarkersPredicate = this.markersPredicateFactory(
this.conflictingChanges.length,
-1,
sectionSize(Section.MERGE_CONFLICTS)
);
- isFirstNonEmpty =
- !firstNonEmptySectionFound && !!this.conflictingChanges?.length;
- firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
- const mergeConflictsSection = html`<section
- id="mergeConflicts"
- ?hidden=${!this.conflictingChanges?.length}
- >
+ return html`<section id="mergeConflicts">
<gr-related-collapse
title="Merge conflicts"
- class="${classMap({first: isFirstNonEmpty})}"
+ class="${classMap({first: isFirst})}"
.length=${this.conflictingChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.MERGE_CONFLICTS)}
>
@@ -340,22 +381,24 @@
)}
</gr-related-collapse>
</section>`;
+ }
+ private renderCherryPicks(
+ isFirst: boolean,
+ sectionSize: (section: Section) => number
+ ) {
+ if (!this.cherryPickChanges.length) {
+ return undefined;
+ }
const cherryPicksMarkersPredicate = this.markersPredicateFactory(
this.cherryPickChanges.length,
-1,
sectionSize(Section.CHERRY_PICKS)
);
- isFirstNonEmpty =
- !firstNonEmptySectionFound && !!this.cherryPickChanges?.length;
- firstNonEmptySectionFound = firstNonEmptySectionFound || isFirstNonEmpty;
- const cherryPicksSection = html`<section
- id="cherryPicks"
- ?hidden=${!this.cherryPickChanges?.length}
- >
+ return html`<section id="cherryPicks">
<gr-related-collapse
title="Cherry picks"
- class="${classMap({first: isFirstNonEmpty})}"
+ class="${classMap({first: isFirst})}"
.length=${this.cherryPickChanges.length}
.numChangesWhenCollapsed=${sectionSize(Section.CHERRY_PICKS)}
>
@@ -382,17 +425,17 @@
)}
</gr-related-collapse>
</section>`;
+ }
- return html`<gr-endpoint-decorator name="related-changes-section">
- <gr-endpoint-param
- name="change"
- .value=${this.change}
- ></gr-endpoint-param>
- <gr-endpoint-slot name="top"></gr-endpoint-slot>
- ${relatedChangeSection} ${submittedTogetherSection} ${sameTopicSection}
- ${mergeConflictsSection} ${cherryPicksSection}
- <gr-endpoint-slot name="bottom"></gr-endpoint-slot>
- </gr-endpoint-decorator>`;
+ private renderChangeTitle(change: ChangeInfo) {
+ return `${change.project}: ${change.branch}: ${change.subject}`;
+ }
+
+ private renderChangeLine(change: ChangeInfo) {
+ const truncatedRepo = truncatePath(change.project, 2);
+ return html`<span class="truncatedRepo" .title="${change.project}"
+ >${truncatedRepo}</span
+ >: ${change.branch}: ${change.subject}`;
}
sectionSizeFactory(
@@ -577,7 +620,10 @@
this.restApiService.getConfig().then(config => {
if (config && !config.change.submit_whole_topic) {
return this.restApiService
- .getChangesWithSameTopic(changeTopic, change._number)
+ .getChangesWithSameTopic(changeTopic, {
+ openChangesOnly: true,
+ changeToExclude: change._number,
+ })
.then(response => {
if (changeTopic === this.change?.topic) {
this.sameTopicChanges = response ?? [];
@@ -682,7 +728,7 @@
@property()
numChangesWhenCollapsed = DEFALT_NUM_CHANGES_WHEN_COLLAPSED;
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
static override get styles() {
return [
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 a6dc338f..94b8668 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
@@ -30,6 +30,7 @@
createSubmittedTogetherInfo,
} from '../../../test/test-data-generators';
import {
+ query,
queryAndAssert,
resetPlugins,
stubRestApi,
@@ -46,7 +47,6 @@
} from '../../../types/common';
import {ParsedChangeInfo} from '../../../types/types';
import {GrEndpointDecorator} from '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit';
import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import './gr-related-changes-list';
import {
@@ -56,8 +56,6 @@
Section,
} from './gr-related-changes-list';
-const pluginApi = _testOnly_initGerritPluginApi();
-
const basicFixture = fixtureFromElement('gr-related-changes-list');
suite('gr-related-changes-list', () => {
@@ -227,11 +225,8 @@
Promise.resolve(submittedTogether)
);
await element.reload();
- const relatedChanges = queryAndAssert<GrRelatedCollapse>(
- queryAndAssert<HTMLElement>(element, '#relatedChanges'),
- 'gr-related-collapse'
- );
- assert.isFalse(relatedChanges!.classList.contains('first'));
+ const relatedChanges = query<HTMLElement>(element, '#relatedChanges');
+ assert.notExists(relatedChanges);
const submittedTogetherSection = queryAndAssert<GrRelatedCollapse>(
queryAndAssert<HTMLElement>(element, '#submittedTogether'),
'gr-related-collapse'
@@ -255,11 +250,11 @@
'gr-related-collapse'
);
assert.isTrue(relatedChanges!.classList.contains('first'));
- const submittedTogetherSection = queryAndAssert<GrRelatedCollapse>(
- queryAndAssert<HTMLElement>(element, '#submittedTogether'),
- 'gr-related-collapse'
+ const submittedTogetherSection = query<HTMLElement>(
+ element,
+ '#submittedTogether'
);
- assert.isFalse(submittedTogetherSection!.classList.contains('first'));
+ assert.notExists(submittedTogetherSection);
const cherryPicks = queryAndAssert<GrRelatedCollapse>(
queryAndAssert<HTMLElement>(element, '#cherryPicks'),
'gr-related-collapse'
@@ -608,7 +603,7 @@
}
let hookEl: RelatedChangesListGrEndpointDecorator;
let plugin: PluginApi;
- pluginApi.install(
+ window.Gerrit.install(
p => {
plugin = p;
plugin
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
index 486f37a..8e78d4e 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
@@ -19,11 +19,9 @@
import './gr-reply-dialog.js';
import {queryAndAssert, resetPlugins, stubRestApi} from '../../../test/test-utils.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
const basicFixture = fixtureFromElement('gr-reply-dialog');
-const pluginApi = _testOnly_initGerritPluginApi();
suite('gr-reply-dialog-it tests', () => {
let element;
@@ -101,7 +99,7 @@
test('lgtm plugin', async () => {
resetPlugins();
- pluginApi.install(plugin => {
+ window.Gerrit.install(plugin => {
const replyApi = plugin.changeReply();
replyApi.addReplyTextChangedCallback(text => {
const label = 'Code-Review';
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 b8932e5..1379c7c 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
@@ -16,6 +16,8 @@
*/
import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param';
+import '../../plugins/gr-endpoint-slot/gr-endpoint-slot';
import '../../shared/gr-account-chip/gr-account-chip';
import '../../shared/gr-textarea/gr-textarea';
import '../../shared/gr-button/gr-button';
@@ -31,7 +33,7 @@
GrReviewerSuggestionsProvider,
SUGGESTIONS_PROVIDERS_USERS_TYPES,
} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {
ChangeStatus,
DraftsAction,
@@ -116,6 +118,7 @@
import {Interaction, Timing} from '../../../constants/reporting';
import {getReplyByReason} from '../../../utils/attention-set-util';
import {addShortcut, Key, Modifier} from '../../../utils/dom-util';
+import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
@@ -215,9 +218,9 @@
FocusTarget = FocusTarget;
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
- private readonly changeService = appContext.changeService;
+ private readonly changeModel = getAppContext().changeModel;
@property({type: Object})
change?: ChangeInfo;
@@ -240,9 +243,6 @@
@property({type: String, observer: '_draftChanged'})
draft = '';
- @property({type: String})
- quote = '';
-
@property({type: Object})
filterReviewerSuggestion: (input: Suggestion) => boolean;
@@ -357,11 +357,12 @@
@property({type: Array, computed: '_computeAllReviewers(_reviewers.*)'})
_allReviewers: (AccountInfo | GroupInfo)[] = [];
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService: RestApiService =
+ getAppContext().restApiService;
- private readonly storage = appContext.storageService;
+ private readonly storage = getAppContext().storageService;
- private readonly jsAPI = appContext.jsApiService;
+ private readonly jsAPI = getAppContext().jsApiService;
private storeTask?: DelayedTask;
@@ -427,20 +428,25 @@
super.disconnectedCallback();
}
- open(focusTarget?: FocusTarget) {
+ /**
+ * Note that this method is not actually *opening* the dialog. Opening and
+ * showing the dialog is dealt with by the overlay. This method is used by the
+ * change view for initializing the dialog after opening the overlay. Maybe it
+ * should be called `onOpened()` or `initialize()`?
+ */
+ open(focusTarget?: FocusTarget, quote?: string) {
assertIsDefined(this.change, 'change');
this.knownLatestState = LatestPatchState.CHECKING;
- this.changeService.fetchChangeUpdates(this.change).then(result => {
+ this.changeModel.fetchChangeUpdates(this.change).then(result => {
this.knownLatestState = result.isLatest
? LatestPatchState.LATEST
: LatestPatchState.NOT_LATEST;
});
this._focusOn(focusTarget);
- if (this.quote && this.quote.length) {
- // If a reply quote has been provided, use it and clear the property.
- this.draft = this.quote;
- this.quote = '';
+ if (quote?.length) {
+ // If a reply quote has been provided, use it.
+ this.draft = quote;
} else {
// Otherwise, check for an unsaved draft in localstorage.
this.draft = this._loadStoredDraft();
@@ -478,24 +484,19 @@
}
setLabelValue(label: string, value: string) {
- const selectorEl = this.getLabelScores().shadowRoot?.querySelector(
- `gr-label-score-row[name="${label}"]`
- );
- if (!selectorEl) {
- return;
- }
- (selectorEl as GrLabelScoreRow).setSelectedValue(value);
+ const selectorEl =
+ this.getLabelScores().shadowRoot?.querySelector<GrLabelScoreRow>(
+ `gr-label-score-row[name="${label}"]`
+ );
+ selectorEl?.setSelectedValue(value);
}
getLabelValue(label: string) {
- const selectorEl = this.getLabelScores().shadowRoot?.querySelector(
- `gr-label-score-row[name="${label}"]`
- );
- if (!selectorEl) {
- return null;
- }
-
- return (selectorEl as GrLabelScoreRow).selectedValue;
+ const selectorEl =
+ this.getLabelScores().shadowRoot?.querySelector<GrLabelScoreRow>(
+ `gr-label-score-row[name="${label}"]`
+ );
+ return selectorEl?.selectedValue;
}
@observe('_ccs.splices')
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
index 45a55c1..a0c6b83 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
@@ -341,7 +341,6 @@
class="message newReplyDialog"
autocomplete="on"
placeholder="[[_messagePlaceholder]]"
- fixed-position-dropdown=""
monospace="true"
disabled="{{disabled}}"
rows="4"
@@ -395,9 +394,6 @@
id="commentList"
hidden$="[[!_includeComments]]"
threads="[[draftCommentThreads]]"
- change="[[change]]"
- change-num="[[change._number]]"
- logged-in="true"
hide-dropdown=""
>
</gr-thread-list>
@@ -409,124 +405,111 @@
</span>
</section>
<div class$="stickyBottom newReplyDialog">
- <section
- hidden$="[[!_showAttentionSummary(_attentionExpanded)]]"
- class="attention"
- >
- <div class="attentionSummary">
- <div>
- <template
- is="dom-if"
- if="[[_computeShowNoAttentionUpdate(serverConfig, _currentAttentionSet, _newAttentionSet, _sendDisabled)]]"
- >
- <span
- >[[_computeDoNotUpdateMessage(_currentAttentionSet,
- _newAttentionSet, _sendDisabled)]]</span
- >
- </template>
- <template
- is="dom-if"
- if="[[!_computeShowNoAttentionUpdate(serverConfig, _currentAttentionSet, _newAttentionSet, _sendDisabled)]]"
- >
- <span>Bring to attention of</span>
+ <gr-endpoint-decorator name="reply-bottom">
+ <gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
+ <section
+ hidden$="[[!_showAttentionSummary(_attentionExpanded)]]"
+ class="attention"
+ >
+ <div class="attentionSummary">
+ <div>
<template
- is="dom-repeat"
- items="[[_computeNewAttentionAccounts(serverConfig, _currentAttentionSet, _newAttentionSet)]]"
- as="account"
+ is="dom-if"
+ if="[[_computeShowNoAttentionUpdate(serverConfig, _currentAttentionSet, _newAttentionSet, _sendDisabled)]]"
>
- <gr-account-label
- account="[[account]]"
- force-attention="[[_computeHasNewAttention(account, _newAttentionSet)]]"
- selected="[[_computeHasNewAttention(account, _newAttentionSet)]]"
- hideHovercard
- selectionChipStyle
- on-click="_handleAttentionClick"
- ></gr-account-label>
+ <span
+ >[[_computeDoNotUpdateMessage(_currentAttentionSet,
+ _newAttentionSet, _sendDisabled)]]</span
+ >
</template>
- </template>
- <gr-tooltip-content
- has-tooltip
- title="[[_computeAttentionButtonTitle(_sendDisabled)]]"
- >
- <gr-button
- class="edit-attention-button"
- on-click="_handleAttentionModify"
- disabled="[[_sendDisabled]]"
- link=""
- position-below=""
- data-label="Edit"
- data-action-type="change"
- data-action-key="edit"
- role="button"
- tabindex="0"
+ <template
+ is="dom-if"
+ if="[[!_computeShowNoAttentionUpdate(serverConfig, _currentAttentionSet, _newAttentionSet, _sendDisabled)]]"
>
- <iron-icon icon="gr-icons:edit"></iron-icon>
- Modify
- </gr-button>
- </gr-tooltip-content>
+ <span>Bring to attention of</span>
+ <template
+ is="dom-repeat"
+ items="[[_computeNewAttentionAccounts(serverConfig, _currentAttentionSet, _newAttentionSet)]]"
+ as="account"
+ >
+ <gr-account-label
+ account="[[account]]"
+ force-attention="[[_computeHasNewAttention(account, _newAttentionSet)]]"
+ selected="[[_computeHasNewAttention(account, _newAttentionSet)]]"
+ hideHovercard
+ selectionChipStyle
+ on-click="_handleAttentionClick"
+ ></gr-account-label>
+ </template>
+ </template>
+ <gr-tooltip-content
+ has-tooltip
+ title="[[_computeAttentionButtonTitle(_sendDisabled)]]"
+ >
+ <gr-button
+ class="edit-attention-button"
+ on-click="_handleAttentionModify"
+ disabled="[[_sendDisabled]]"
+ link=""
+ position-below=""
+ data-label="Edit"
+ data-action-type="change"
+ data-action-key="edit"
+ role="button"
+ tabindex="0"
+ >
+ <iron-icon icon="gr-icons:edit"></iron-icon>
+ Modify
+ </gr-button>
+ </gr-tooltip-content>
+ </div>
+ <div>
+ <a
+ href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
+ target="_blank"
+ >
+ <iron-icon
+ icon="gr-icons:help-outline"
+ title="read documentation"
+ ></iron-icon>
+ </a>
+ </div>
</div>
- <div>
- <a
- href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
- target="_blank"
+ </section>
+ <section
+ hidden$="[[!_showAttentionDetails(_attentionExpanded)]]"
+ class="attention-detail"
+ >
+ <div class="attentionDetailsTitle">
+ <div>
+ <span>Modify attention to</span>
+ </div>
+ <div></div>
+ <div>
+ <a
+ href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
+ target="_blank"
+ >
+ <iron-icon
+ icon="gr-icons:help-outline"
+ title="read documentation"
+ ></iron-icon>
+ </a>
+ </div>
+ </div>
+ <div class="selectUsers">
+ <span
+ >Select chips to set who will be in the attention set after sending
+ this reply</span
>
- <iron-icon
- icon="gr-icons:help-outline"
- title="read documentation"
- ></iron-icon>
- </a>
</div>
- </div>
- </section>
- <section
- hidden$="[[!_showAttentionDetails(_attentionExpanded)]]"
- class="attention-detail"
- >
- <div class="attentionDetailsTitle">
- <div>
- <span>Modify attention to</span>
- </div>
- <div></div>
- <div>
- <a
- href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
- target="_blank"
- >
- <iron-icon
- icon="gr-icons:help-outline"
- title="read documentation"
- ></iron-icon>
- </a>
- </div>
- </div>
- <div class="selectUsers">
- <span
- >Select chips to set who will be in the attention set after sending
- this reply</span
- >
- </div>
- <div class="peopleList">
- <div class="peopleListLabel">Owner</div>
- <div class="peopleListValues">
- <gr-account-label
- account="[[_owner]]"
- force-attention="[[_computeHasNewAttention(_owner, _newAttentionSet)]]"
- selected="[[_computeHasNewAttention(_owner, _newAttentionSet)]]"
- hideHovercard
- selectionChipStyle
- on-click="_handleAttentionClick"
- >
- </gr-account-label>
- </div>
- </div>
- <template is="dom-if" if="[[_uploader]]">
<div class="peopleList">
- <div class="peopleListLabel">Uploader</div>
+ <div class="peopleListLabel">Owner</div>
<div class="peopleListValues">
<gr-account-label
- account="[[_uploader]]"
- force-attention="[[_computeHasNewAttention(_uploader, _newAttentionSet)]]"
- selected="[[_computeHasNewAttention(_uploader, _newAttentionSet)]]"
+ account="[[_owner]]"
+ force-attention="[[_computeHasNewAttention(_owner, _newAttentionSet)]]"
+ selected="[[_computeHasNewAttention(_owner, _newAttentionSet)]]"
hideHovercard
selectionChipStyle
on-click="_handleAttentionClick"
@@ -534,34 +517,28 @@
</gr-account-label>
</div>
</div>
- </template>
- <div class="peopleList">
- <div class="peopleListLabel">Reviewers</div>
- <div class="peopleListValues">
- <template
- is="dom-repeat"
- items="[[_removeServiceUsers(_reviewers, _newAttentionSet)]]"
- as="account"
- >
- <gr-account-label
- account="[[account]]"
- force-attention="[[_computeHasNewAttention(account, _newAttentionSet)]]"
- selected="[[_computeHasNewAttention(account, _newAttentionSet)]]"
- hideHovercard
- selectionChipStyle
- on-click="_handleAttentionClick"
- >
- </gr-account-label>
- </template>
- </div>
- </div>
- <template is="dom-if" if="[[_attentionCcsCount]]">
+ <template is="dom-if" if="[[_uploader]]">
+ <div class="peopleList">
+ <div class="peopleListLabel">Uploader</div>
+ <div class="peopleListValues">
+ <gr-account-label
+ account="[[_uploader]]"
+ force-attention="[[_computeHasNewAttention(_uploader, _newAttentionSet)]]"
+ selected="[[_computeHasNewAttention(_uploader, _newAttentionSet)]]"
+ hideHovercard
+ selectionChipStyle
+ on-click="_handleAttentionClick"
+ >
+ </gr-account-label>
+ </div>
+ </div>
+ </template>
<div class="peopleList">
- <div class="peopleListLabel">CC</div>
+ <div class="peopleListLabel">Reviewers</div>
<div class="peopleListValues">
<template
is="dom-repeat"
- items="[[_removeServiceUsers(_ccs, _newAttentionSet)]]"
+ items="[[_removeServiceUsers(_reviewers, _newAttentionSet)]]"
as="account"
>
<gr-account-label
@@ -576,76 +553,99 @@
</template>
</div>
</div>
- </template>
- <template
- is="dom-if"
- if="[[_computeShowAttentionTip(_account, _owner, _currentAttentionSet, _newAttentionSet)]]"
- >
- <div class="attentionTip">
- <iron-icon
- class="pointer"
- icon="gr-icons:lightbulb-outline"
- ></iron-icon>
- Be mindful of requiring attention from too many users.
+ <template is="dom-if" if="[[_attentionCcsCount]]">
+ <div class="peopleList">
+ <div class="peopleListLabel">CC</div>
+ <div class="peopleListValues">
+ <template
+ is="dom-repeat"
+ items="[[_removeServiceUsers(_ccs, _newAttentionSet)]]"
+ as="account"
+ >
+ <gr-account-label
+ account="[[account]]"
+ force-attention="[[_computeHasNewAttention(account, _newAttentionSet)]]"
+ selected="[[_computeHasNewAttention(account, _newAttentionSet)]]"
+ hideHovercard
+ selectionChipStyle
+ on-click="_handleAttentionClick"
+ >
+ </gr-account-label>
+ </template>
+ </div>
+ </div>
+ </template>
+ <template
+ is="dom-if"
+ if="[[_computeShowAttentionTip(_account, _owner, _currentAttentionSet, _newAttentionSet)]]"
+ >
+ <div class="attentionTip">
+ <iron-icon
+ class="pointer"
+ icon="gr-icons:lightbulb-outline"
+ ></iron-icon>
+ Be mindful of requiring attention from too many users.
+ </div>
+ </template>
+ </section>
+ <gr-endpoint-slot name="above-actions"></gr-endpoint-slot>
+ <section class="actions">
+ <div class="left">
+ <span
+ id="checkingStatusLabel"
+ hidden$="[[!_isState(knownLatestState, 'checking')]]"
+ >
+ Checking whether patch [[patchNum]] is latest...
+ </span>
+ <span
+ id="notLatestLabel"
+ hidden$="[[!_isState(knownLatestState, 'not-latest')]]"
+ >
+ [[_computePatchSetWarning(patchNum, _labelsChanged)]]
+ <gr-button link="" on-click="_reload">Reload</gr-button>
+ </span>
</div>
- </template>
- </section>
- <section class="actions">
- <div class="left">
- <span
- id="checkingStatusLabel"
- hidden$="[[!_isState(knownLatestState, 'checking')]]"
- >
- Checking whether patch [[patchNum]] is latest...
- </span>
- <span
- id="notLatestLabel"
- hidden$="[[!_isState(knownLatestState, 'not-latest')]]"
- >
- [[_computePatchSetWarning(patchNum, _labelsChanged)]]
- <gr-button link="" on-click="_reload">Reload</gr-button>
- </span>
- </div>
- <div class="right">
- <gr-button
- link=""
- id="cancelButton"
- class="action cancel"
- on-click="_cancelTapHandler"
- >Cancel</gr-button
- >
- <template is="dom-if" if="[[canBeStarted]]">
- <!-- Use 'Send' here as the change may only about reviewers / ccs
- and when this button is visible, the next button will always
- be 'Start review' -->
+ <div class="right">
+ <gr-button
+ link=""
+ id="cancelButton"
+ class="action cancel"
+ on-click="_cancelTapHandler"
+ >Cancel</gr-button
+ >
+ <template is="dom-if" if="[[canBeStarted]]">
+ <!-- Use 'Send' here as the change may only about reviewers / ccs
+ and when this button is visible, the next button will always
+ be 'Start review' -->
+ <gr-tooltip-content
+ has-tooltip=""
+ title$="[[_saveTooltip]]"
+ >
+ <gr-button
+ link=""
+ disabled="[[_isState(knownLatestState, 'not-latest')]]"
+ class="action save"
+ on-click="_saveClickHandler"
+ >Send As WIP</gr-button
+ >
+ </gr-tooltip-content>
+ </template>
<gr-tooltip-content
has-tooltip=""
- title$="[[_saveTooltip]]"
+ title$="[[_computeSendButtonTooltip(canBeStarted, _commentEditing)]]"
>
<gr-button
- link=""
- disabled="[[_isState(knownLatestState, 'not-latest')]]"
- class="action save"
- on-click="_saveClickHandler"
- >Send As WIP</gr-button
- >
+ id="sendButton"
+ primary=""
+ disabled="[[_sendDisabled]]"
+ class="action send"
+ on-click="_sendTapHandler"
+ >[[_sendButtonLabel]]
+ </gr-button>
</gr-tooltip-content>
- </template>
- <gr-tooltip-content
- has-tooltip=""
- title$="[[_computeSendButtonTooltip(canBeStarted, _commentEditing)]]"
- >
- <gr-button
- id="sendButton"
- primary=""
- disabled="[[_sendDisabled]]"
- class="action send"
- on-click="_sendTapHandler"
- >[[_sendButtonLabel]]
- </gr-button>
- </gr-tooltip-content>
- </div>
- </section>
+ </div>
+ </section>
+ </gr-endpoint-decorator>
</div>
</div>
`;
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 5a11f47..14ad4ad 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
@@ -18,9 +18,11 @@
import '../../../test/common-test-setup-karma';
import './gr-reply-dialog';
import {
+ addListenerForTest,
mockPromise,
queryAll,
queryAndAssert,
+ stubRestApi,
stubStorage,
} from '../../../test/test-utils';
import {
@@ -28,14 +30,12 @@
ReviewerState,
SpecialFilePath,
} from '../../../constants/constants';
-import {appContext} from '../../../services/app-context';
-import {addListenerForTest} from '../../../test/test-utils';
-import {stubRestApi} from '../../../test/test-utils';
import {JSON_PREFIX} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
import {StandardLabels} from '../../../utils/label-util';
import {
createAccountWithId,
createChange,
+ createComment,
createCommentThread,
createDraft,
createRevision,
@@ -44,7 +44,7 @@
pressAndReleaseKeyOn,
tap,
} from '@polymer/iron-test-helpers/mock-interactions';
-import {GrReplyDialog} from './gr-reply-dialog';
+import {FocusTarget, GrReplyDialog} from './gr-reply-dialog';
import {
AccountId,
AccountInfo,
@@ -123,8 +123,6 @@
stubRestApi('getChange').returns(Promise.resolve({...createChange()}));
stubRestApi('getChangeSuggestedReviewers').returns(Promise.resolve([]));
- sinon.stub(appContext.flagsService, 'isEnabled').returns(true);
-
element = basicFixture.instantiate();
element.change = {
...createChange(),
@@ -321,18 +319,13 @@
if (hasDraft) {
draftThreads = [
{
- ...createCommentThread([
- {
- ...createDraft(),
- __draft: true,
- unresolved: true,
- },
- ]),
+ ...createCommentThread([{...createDraft(), unresolved: true}]),
},
];
}
replyToIds?.forEach(id =>
draftThreads[0].comments.push({
+ ...createComment(),
author: {_account_id: id},
})
);
@@ -881,11 +874,13 @@
{
...createCommentThread([
{
+ ...createComment(),
id: '1' as UrlEncodedCommentId,
author: {_account_id: 1 as AccountId},
unresolved: false,
},
{
+ ...createComment(),
id: '2' as UrlEncodedCommentId,
in_reply_to: '1' as UrlEncodedCommentId,
author: {_account_id: 2 as AccountId},
@@ -896,11 +891,13 @@
{
...createCommentThread([
{
+ ...createComment(),
id: '3' as UrlEncodedCommentId,
author: {_account_id: 3 as AccountId},
unresolved: false,
},
{
+ ...createComment(),
id: '4' as UrlEncodedCommentId,
in_reply_to: '3' as UrlEncodedCommentId,
author: {_account_id: 4 as AccountId},
@@ -1317,11 +1314,9 @@
const storedDraft = 'hello world';
const quote = '> foo bar';
getDraftCommentStub.returns({message: storedDraft});
- element.quote = quote;
- element.open();
+ element.open(FocusTarget.ANY, quote);
assert.isFalse(getDraftCommentStub.called);
assert.equal(element.draft, quote);
- assert.isNotOk(element.quote);
});
test('updates stored draft on edits', async () => {
@@ -2008,7 +2003,7 @@
element._computeSendButtonDisabled(
/* canBeStarted= */ false,
/* draftCommentThreads= */ [
- {...createCommentThread([{__draft: true}])},
+ {...createCommentThread([createComment()])},
],
/* text= */ '',
/* reviewersMutated= */ false,
@@ -2028,7 +2023,7 @@
element._computeSendButtonDisabled(
/* canBeStarted= */ false,
/* draftCommentThreads= */ [
- {...createCommentThread([{__draft: true}])},
+ {...createCommentThread([createComment()])},
],
/* text= */ '',
/* reviewersMutated= */ false,
@@ -2047,7 +2042,9 @@
assert.isFalse(
element._computeSendButtonDisabled(
/* canBeStarted= */ false,
- /* draftCommentThreads= */ [{...createCommentThread([{}])}],
+ /* draftCommentThreads= */ [
+ {...createCommentThread([createComment()])},
+ ],
/* text= */ 'test',
/* reviewersMutated= */ false,
/* labelsChanged= */ false,
@@ -2065,7 +2062,9 @@
assert.isFalse(
element._computeSendButtonDisabled(
/* canBeStarted= */ false,
- /* draftCommentThreads= */ [{...createCommentThread([{}])}],
+ /* draftCommentThreads= */ [
+ {...createCommentThread([createComment()])},
+ ],
/* text= */ '',
/* reviewersMutated= */ true,
/* labelsChanged= */ false,
@@ -2083,7 +2082,9 @@
assert.isFalse(
element._computeSendButtonDisabled(
/* canBeStarted= */ false,
- /* draftCommentThreads= */ [{...createCommentThread([{}])}],
+ /* draftCommentThreads= */ [
+ {...createCommentThread([createComment()])},
+ ],
/* text= */ '',
/* reviewersMutated= */ false,
/* labelsChanged= */ true,
@@ -2101,7 +2102,9 @@
assert.isTrue(
element._computeSendButtonDisabled(
/* canBeStarted= */ false,
- /* draftCommentThreads= */ [{...createCommentThread([{}])}],
+ /* draftCommentThreads= */ [
+ {...createCommentThread([createComment()])},
+ ],
/* text= */ '',
/* reviewersMutated= */ false,
/* labelsChanged= */ true,
@@ -2125,7 +2128,9 @@
assert.isFalse(
element._computeSendButtonDisabled(
/* canBeStarted= */ false,
- /* draftCommentThreads= */ [{...createCommentThread([{}])}],
+ /* draftCommentThreads= */ [
+ {...createCommentThread([createComment()])},
+ ],
/* text= */ '',
/* reviewersMutated= */ false,
/* labelsChanged= */ false,
@@ -2149,7 +2154,12 @@
element.draftCommentThreads = [
{
...createCommentThread([
- {__draft: true, path: 'test', line: 1, patch_set: 1 as PatchSetNum},
+ {
+ ...createDraft(),
+ path: 'test',
+ line: 1,
+ patch_set: 1 as PatchSetNum,
+ },
]),
},
];
@@ -2172,7 +2182,12 @@
element.draftCommentThreads = [
{
...createCommentThread([
- {__draft: true, path: 'test', line: 1, patch_set: 1 as PatchSetNum},
+ {
+ ...createDraft(),
+ path: 'test',
+ line: 1,
+ patch_set: 1 as PatchSetNum,
+ },
]),
},
];
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
index de11b16..b236173 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.ts
@@ -39,9 +39,13 @@
import {hasOwnProperty} from '../../../utils/common-util';
import {isRemovableReviewer} from '../../../utils/change-util';
import {ReviewerState} from '../../../constants/constants';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {fireAlert} from '../../../utils/event-util';
-import {getApprovalInfo, getCodeReviewLabel} from '../../../utils/label-util';
+import {
+ getApprovalInfo,
+ getCodeReviewLabel,
+ showNewSubmitRequirements,
+} from '../../../utils/label-util';
import {sortReviewers} from '../../../utils/attention-set-util';
@customElement('gr-reviewer-list')
@@ -86,7 +90,9 @@
@property({type: Object})
_xhrPromise?: Promise<Response | undefined>;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
+
+ private readonly flagsService = getAppContext().flagsService;
@computed('ccsOnly')
get _addLabel() {
@@ -129,21 +135,6 @@
}
/**
- * Returns hash of labels to max permitted score.
- *
- * @returns labels to max permitted scores hash
- */
- _getMaxPermittedScores(change: ChangeInfo) {
- return this._permittedLabelsToNumericScores(change.permitted_labels)
- .map(({label, scores}) => {
- return {
- [label]: scores.reduce((a, b) => Math.max(a, b)),
- };
- })
- .reduce((acc, i) => Object.assign(acc, i), {});
- }
-
- /**
* Returns max permitted score for reviewer.
*/
_getReviewerPermittedScore(
@@ -179,17 +170,13 @@
return '';
}
const maxScores = [];
- const maxPermitted = this._getMaxPermittedScores(change);
for (const label of Object.keys(change.labels)) {
const maxScore = this._getReviewerPermittedScore(reviewer, change, label);
if (isNaN(maxScore) || maxScore < 0) {
continue;
}
- if (maxScore > 0 && maxScore === maxPermitted[label]) {
- maxScores.push(`${label}: +${maxScore}`);
- } else {
- maxScores.push(`${label}`);
- }
+ const scoreLabel = maxScore > 0 ? `+${maxScore}` : `${maxScore}`;
+ maxScores.push(`${label}: ${scoreLabel}`);
}
return maxScores.join(', ');
}
@@ -321,6 +308,10 @@
if (!this.change) return Promise.resolve(undefined);
return this.restApiService.removeChangeReviewer(this.change._number, id);
}
+
+ showNewSubmitRequirements(change?: ChangeInfo) {
+ return showNewSubmitRequirements(this.flagsService, change);
+ }
}
declare global {
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
index dec65e2..dfe69a6 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.ts
@@ -73,11 +73,13 @@
voteable-text="[[_computeVoteableText(reviewer, change)]]"
removable="[[_computeCanRemoveReviewer(reviewer, mutable)]]"
>
- <gr-vote-chip
- slot="vote-chip"
- vote="[[_computeVote(reviewer, change)]]"
- label="[[_computeCodeReviewLabel(change)]]"
- ></gr-vote-chip>
+ <template is="dom-if" if="[[showNewSubmitRequirements(change)]]">
+ <gr-vote-chip
+ slot="vote-chip"
+ vote="[[_computeVote(reviewer, change)]]"
+ label="[[_computeCodeReviewLabel(change)]]"
+ ></gr-vote-chip>
+ </template>
</gr-account-chip>
</template>
<div class="controlsContainer" hidden$="[[!mutable]]">
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.ts b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.ts
index bf15bb5..a5bcc3a 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.ts
@@ -457,11 +457,11 @@
};
assert.strictEqual(
element._computeVoteableText({...createAccountDetailWithId(1)}, change),
- 'Bar'
+ 'Bar: +1'
);
assert.strictEqual(
element._computeVoteableText({...createAccountDetailWithId(7)}, change),
- 'Foo: +2, Bar, FooBar'
+ 'Foo: +2, Bar: +1, FooBar: 0'
);
assert.strictEqual(
element._computeVoteableText({...createAccountDetailWithId(2)}, change),
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-dashboard-hovercard/gr-submit-requirement-dashboard-hovercard.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-dashboard-hovercard/gr-submit-requirement-dashboard-hovercard.ts
new file mode 100644
index 0000000..72f04e3
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-dashboard-hovercard/gr-submit-requirement-dashboard-hovercard.ts
@@ -0,0 +1,59 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../gr-submit-requirements/gr-submit-requirements';
+import {customElement, property} from 'lit/decorators';
+import {css, html, LitElement} from 'lit';
+import {HovercardMixin} from '../../../mixins/hovercard-mixin/hovercard-mixin';
+import {ParsedChangeInfo} from '../../../types/types';
+
+// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
+const base = HovercardMixin(LitElement);
+
+@customElement('gr-submit-requirement-dashboard-hovercard')
+export class GrSubmitRequirementDashboardHovercard extends base {
+ @property({type: Object})
+ change?: ParsedChangeInfo;
+
+ static override get styles() {
+ return [
+ base.styles || [],
+ css`
+ #container {
+ padding: var(--spacing-xl);
+ padding-left: var(--spacing-s);
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`<div id="container" role="tooltip" tabindex="-1">
+ <gr-submit-requirements
+ .change=${this.change}
+ disable-hovercards
+ suppress-title
+ disable-endpoints
+ ></gr-submit-requirements>
+ </div>`;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-submit-requirement-dashboard-hovercard': GrSubmitRequirementDashboardHovercard;
+ }
+}
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
index 002ac56..decbe93 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
@@ -19,18 +19,30 @@
import {customElement, property} from 'lit/decorators';
import {
AccountInfo,
+ ChangeStatus,
+ isDetailedLabelInfo,
SubmitRequirementExpressionInfo,
SubmitRequirementResultInfo,
+ SubmitRequirementStatus,
} from '../../../api/rest-api';
import {
+ canVote,
extractAssociatedLabels,
+ getApprovalInfo,
+ hasVotes,
iconForStatus,
} from '../../../utils/label-util';
import {ParsedChangeInfo} from '../../../types/types';
-import {Label} from '../gr-change-requirements/gr-change-requirements';
import {css, html, LitElement} from 'lit';
import {HovercardMixin} from '../../../mixins/hovercard-mixin/hovercard-mixin';
import {fontStyles} from '../../../styles/gr-font-styles';
+import {DraftsAction} from '../../../constants/constants';
+import {ReviewInput} from '../../../types/common';
+import {getAppContext} from '../../../services/app-context';
+import {assertIsDefined} from '../../../utils/common-util';
+import {CURRENT} from '../../../utils/patch-set-util';
+import {fireReload} from '../../../utils/event-util';
+import {submitRequirementsStyles} from '../../../styles/gr-submit-requirements-styles';
// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
const base = HovercardMixin(LitElement);
@@ -52,9 +64,12 @@
@property({type: Boolean})
expanded = false;
+ private readonly restApiService = getAppContext().restApiService;
+
static override get styles() {
return [
fontStyles,
+ submitRequirementsStyles,
base.styles || [],
css`
#container {
@@ -97,28 +112,28 @@
width: 20px;
height: 20px;
}
- .condition {
+ .section.condition > .sectionContent {
background-color: var(--gray-background);
padding: var(--spacing-m);
flex-grow: 1;
}
+ .button ~ .condition {
+ margin-top: var(--spacing-m);
+ }
.expression {
color: var(--gray-foreground);
}
- iron-icon.check {
- color: var(--success-foreground);
- }
- iron-icon.close {
- color: var(--warning-foreground);
- }
- .showConditions iron-icon {
+ .button iron-icon {
color: inherit;
}
- div.showConditions {
+ div.button {
border-top: 1px solid var(--border-color);
margin-top: var(--spacing-m);
padding: var(--spacing-m) var(--spacing-xl) 0;
}
+ .section.description > .sectionContent {
+ white-space: pre-wrap;
+ }
`,
];
}
@@ -148,12 +163,49 @@
</div>
</div>
</div>
- ${this.renderLabelSection()} ${this.renderConditionSection()}
+ ${this.renderLabelSection()}${this.renderDescription()}
+ ${this.renderShowHideConditionButton()}${this.renderConditionSection()}
+ ${this.renderVotingButtons()}
+ </div>`;
+ }
+
+ private renderDescription() {
+ let description = this.requirement?.description;
+ if (this.requirement?.status === SubmitRequirementStatus.ERROR) {
+ const submitRecord = this.change?.submit_records?.filter(
+ record => record.rule_name === this.requirement?.name
+ );
+ if (submitRecord?.length === 1 && submitRecord[0].error_message) {
+ description = submitRecord[0].error_message;
+ }
+ }
+ if (!description) return;
+ return html`<div class="section description">
+ <div class="sectionIcon">
+ <iron-icon icon="gr-icons:description"></iron-icon>
+ </div>
+ <div class="sectionContent">${description}</div>
</div>`;
}
private renderLabelSection() {
- const labels = this.computeLabels();
+ if (!this.requirement) return;
+ const requirementLabels = extractAssociatedLabels(this.requirement);
+ const allLabels = this.change?.labels ?? {};
+ const labels: string[] = [];
+ for (const label of Object.keys(allLabels)) {
+ if (requirementLabels.includes(label)) {
+ const labelInfo = allLabels[label];
+ const canSomeoneVote = (this.change?.reviewers['REVIEWER'] ?? []).some(
+ reviewer => canVote(labelInfo, reviewer)
+ );
+ if (hasVotes(labelInfo) || canSomeoneVote) {
+ labels.push(label);
+ }
+ }
+ }
+
+ if (labels.length === 0) return;
const showLabelName = labels.length >= 2;
return html` <div class="section">
<div class="sectionIcon"></div>
@@ -163,84 +215,136 @@
</div>`;
}
- private renderLabel(label: Label, showLabelName: boolean) {
+ private renderLabel(labelName: string, showLabelName: boolean) {
+ const labels = this.change?.labels ?? {};
return html`
- ${showLabelName ? html`<div>${label.labelName} votes</div>` : ''}
+ ${showLabelName ? html`<div>${labelName} votes</div>` : ''}
<gr-label-info
.change=${this.change}
.account=${this.account}
.mutable=${this.mutable}
- .label="${label.labelName}"
- .labelInfo="${label.labelInfo}"
+ .label=${labelName}
+ .labelInfo=${labels[labelName]}
></gr-label-info>
`;
}
- private renderConditionSection() {
- if (!this.expanded) {
- return html` <div class="showConditions">
- <gr-button
- link=""
- class="showConditions"
- @click="${(_: MouseEvent) => this.handleShowConditions()}"
- >
- View condition
- <iron-icon icon="gr-icons:expand-more"></iron-icon
- ></gr-button>
- </div>`;
+ private renderShowHideConditionButton() {
+ const buttonText = this.expanded ? 'Hide conditions' : 'View conditions';
+ const icon = this.expanded ? 'expand-less' : 'expand-more';
+
+ return html` <div class="button">
+ <gr-button
+ link=""
+ id="toggleConditionsButton"
+ @click="${(_: MouseEvent) => this.toggleConditionsVisibility()}"
+ >
+ ${buttonText}
+ <iron-icon icon="gr-icons:${icon}"></iron-icon
+ ></gr-button>
+ </div>`;
+ }
+
+ private renderVotingButtons() {
+ if (!this.requirement) return;
+ if (!this.account) return;
+ if (this.change?.status === ChangeStatus.MERGED) return;
+
+ const submittabilityLabels = extractAssociatedLabels(
+ this.requirement,
+ 'onlySubmittability'
+ );
+ const submittabilityVotes = submittabilityLabels.map(labelName =>
+ this.renderLabelVote(labelName, 'submittability')
+ );
+
+ const overrideLabels = extractAssociatedLabels(
+ this.requirement,
+ 'onlyOverride'
+ );
+ const overrideVotes = overrideLabels.map(labelName =>
+ this.renderLabelVote(labelName, 'override')
+ );
+
+ return submittabilityVotes.concat(overrideVotes);
+ }
+
+ private renderLabelVote(
+ labelName: string,
+ type: 'override' | 'submittability'
+ ) {
+ const labels = this.change?.labels ?? {};
+ const labelInfo = labels[labelName];
+ if (!labelInfo || !isDetailedLabelInfo(labelInfo)) return;
+ if (!this.account || !canVote(labelInfo, this.account)) return;
+
+ const approvalInfo = getApprovalInfo(labelInfo, this.account);
+ const maxVote = approvalInfo?.permitted_voting_range?.max;
+ if (!maxVote || maxVote <= 0) return;
+ if (approvalInfo?.value === maxVote) return; // Already voted maxVote
+ return html` <div class="button quickApprove">
+ <gr-button
+ link=""
+ @click="${(_: MouseEvent) => this.quickApprove(labelName, maxVote)}"
+ >
+ ${this.computeVoteButtonName(labelName, maxVote, type)}
+ </gr-button>
+ </div>`;
+ }
+
+ private computeVoteButtonName(
+ labelName: string,
+ maxVote: number,
+ type: 'override' | 'submittability'
+ ) {
+ if (type === 'override') {
+ return `Override (${labelName})`;
} else {
- return html`
- <div class="section">
- <div class="sectionIcon">
- <iron-icon icon="gr-icons:description"></iron-icon>
- </div>
- <div class="sectionContent">${this.requirement?.description}</div>
- </div>
- ${this.renderCondition(
- 'Blocking condition',
- this.requirement?.submittability_expression_result
- )}
- ${this.renderCondition(
- 'Application condition',
- this.requirement?.applicability_expression_result
- )}
- ${this.renderCondition(
- 'Override condition',
- this.requirement?.override_expression_result
- )}
- `;
+ return `Vote ${labelName} +${maxVote}`;
}
}
- private computeLabels() {
- if (!this.requirement) return [];
- const requirementLabels = extractAssociatedLabels(this.requirement);
- const labels = this.change?.labels ?? {};
+ private quickApprove(label: string, score: number) {
+ assertIsDefined(this.change, 'change');
+ const review: ReviewInput = {
+ drafts: DraftsAction.PUBLISH_ALL_REVISIONS,
+ labels: {
+ [label]: score,
+ },
+ };
+ return this.restApiService
+ .saveChangeReview(this.change._number, CURRENT, review)
+ .then(() => {
+ fireReload(this, true);
+ });
+ }
- const allLabels: Label[] = [];
-
- for (const label of Object.keys(labels)) {
- if (requirementLabels.includes(label)) {
- allLabels.push({
- labelName: label,
- icon: '',
- style: '',
- labelInfo: labels[label],
- });
- }
- }
- return allLabels;
+ private renderConditionSection() {
+ if (!this.expanded) return;
+ return html`
+ ${this.renderCondition(
+ 'Submit condition',
+ this.requirement?.submittability_expression_result
+ )}
+ ${this.renderCondition(
+ 'Application condition',
+ this.requirement?.applicability_expression_result
+ )}
+ ${this.renderCondition(
+ 'Override condition',
+ this.requirement?.override_expression_result
+ )}
+ `;
}
private renderCondition(
name: string,
expression?: SubmitRequirementExpressionInfo
) {
- if (!expression) return '';
+ if (!expression?.expression) return '';
return html`
- <div class="section">
- <div class="sectionIcon"></div>
- <div class="sectionContent condition">
+ <div class="section condition">
+ <div class="sectionContent">
${name}:<br />
<span class="expression"> ${expression.expression} </span>
</div>
@@ -248,8 +352,8 @@
`;
}
- private handleShowConditions() {
- this.expanded = true;
+ private toggleConditionsVisibility() {
+ this.expanded = !this.expanded;
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
new file mode 100644
index 0000000..4ff659d
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
@@ -0,0 +1,418 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import {fixture} from '@open-wc/testing-helpers';
+import {html} from 'lit';
+import './gr-submit-requirement-hovercard';
+import {GrSubmitRequirementHovercard} from './gr-submit-requirement-hovercard';
+import {
+ createAccountWithId,
+ createApproval,
+ createChange,
+ createDetailedLabelInfo,
+ createParsedChange,
+ createSubmitRequirementExpressionInfo,
+ createSubmitRequirementResultInfo,
+} from '../../../test/test-data-generators';
+import {ParsedChangeInfo} from '../../../types/types';
+import {query, queryAndAssert} from '../../../test/test-utils';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {ChangeStatus, SubmitRequirementResultInfo} from '../../../api/rest-api';
+
+suite('gr-submit-requirement-hovercard tests', () => {
+ let element: GrSubmitRequirementHovercard;
+
+ setup(async () => {
+ element = await fixture<GrSubmitRequirementHovercard>(
+ html`<gr-submit-requirement-hovercard
+ .requirement="${createSubmitRequirementResultInfo()}"
+ .change=${createChange()}
+ .account="${createAccountWithId()}"
+ ></gr-submit-requirement-hovercard>`
+ );
+ });
+
+ test('renders', async () => {
+ expect(element).shadowDom.to.equal(`<div
+ id="container"
+ role="tooltip"
+ tabindex="-1"
+ >
+ <div class="section">
+ <div class="sectionIcon">
+ <iron-icon
+ class="check-circle-filled"
+ icon="gr-icons:check-circle-filled"
+ >
+ </iron-icon>
+ </div>
+ <div class="sectionContent">
+ <h3 class="heading-3 name">
+ <span>
+ Verified
+ </span>
+ </h3>
+ </div>
+ </div>
+ <div class="section">
+ <div class="sectionIcon">
+ <iron-icon
+ class="small"
+ icon="gr-icons:info-outline"
+ >
+ </iron-icon>
+ </div>
+ <div class="sectionContent">
+ <div class="row">
+ <div class="title">
+ Status
+ </div>
+ <div>
+ SATISFIED
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="button">
+ <gr-button
+ aria-disabled="false"
+ id="toggleConditionsButton"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ View conditions
+ <iron-icon icon="gr-icons:expand-more">
+ </iron-icon>
+ </gr-button>
+ </div>
+ </div>
+ `);
+ });
+
+ test('renders conditions after click', async () => {
+ const button = queryAndAssert<GrButton>(element, '#toggleConditionsButton');
+ button.click();
+ await element.updateComplete;
+ expect(element).shadowDom.to.equal(`<div
+ id="container"
+ role="tooltip"
+ tabindex="-1"
+ >
+ <div class="section">
+ <div class="sectionIcon">
+ <iron-icon
+ class="check-circle-filled"
+ icon="gr-icons:check-circle-filled"
+ >
+ </iron-icon>
+ </div>
+ <div class="sectionContent">
+ <h3 class="heading-3 name">
+ <span>
+ Verified
+ </span>
+ </h3>
+ </div>
+ </div>
+ <div class="section">
+ <div class="sectionIcon">
+ <iron-icon
+ class="small"
+ icon="gr-icons:info-outline"
+ >
+ </iron-icon>
+ </div>
+ <div class="sectionContent">
+ <div class="row">
+ <div class="title">
+ Status
+ </div>
+ <div>
+ SATISFIED
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="button">
+ <gr-button
+ aria-disabled="false"
+ id="toggleConditionsButton"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ Hide conditions
+ <iron-icon icon="gr-icons:expand-less">
+ </iron-icon>
+ </gr-button>
+ </div>
+ <div class="section condition">
+ <div class="sectionContent">
+ Submit condition:
+ <br>
+ <span class="expression">
+ label:Verified=MAX -label:Verified=MIN
+ </span>
+ </div>
+ </div>
+ </div>
+ `);
+ });
+
+ test('renders label', async () => {
+ const submitRequirement: SubmitRequirementResultInfo = {
+ ...createSubmitRequirementResultInfo(),
+ description: 'Test Description',
+ submittability_expression_result: {
+ ...createSubmitRequirementExpressionInfo(),
+ expression: 'label:Verified=MAX -label:Verified=MIN',
+ },
+ };
+ const change: ParsedChangeInfo = {
+ ...createParsedChange(),
+ labels: {
+ Verified: {
+ ...createDetailedLabelInfo(),
+ all: [
+ {
+ ...createApproval(),
+ value: 2,
+ },
+ ],
+ },
+ },
+ };
+ const element = await fixture<GrSubmitRequirementHovercard>(
+ html`<gr-submit-requirement-hovercard
+ .requirement=${submitRequirement}
+ .change=${change}
+ .account=${createAccountWithId()}
+ ></gr-submit-requirement-hovercard>`
+ );
+ expect(element).shadowDom.to.equal(`<div
+ id="container"
+ role="tooltip"
+ tabindex="-1"
+ >
+ <div class="section">
+ <div class="sectionIcon">
+ <iron-icon
+ class="check-circle-filled"
+ icon="gr-icons:check-circle-filled"
+ >
+ </iron-icon>
+ </div>
+ <div class="sectionContent">
+ <h3 class="heading-3 name">
+ <span>
+ Verified
+ </span>
+ </h3>
+ </div>
+ </div>
+ <div class="section">
+ <div class="sectionIcon">
+ <iron-icon
+ class="small"
+ icon="gr-icons:info-outline"
+ >
+ </iron-icon>
+ </div>
+ <div class="sectionContent">
+ <div class="row">
+ <div class="title">
+ Status
+ </div>
+ <div>
+ SATISFIED
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="section">
+ <div class="sectionIcon">
+ </div>
+ <div class="row">
+ <div>
+ <gr-label-info>
+ </gr-label-info>
+ </div>
+ </div>
+ </div>
+ <div class="section description">
+ <div class="sectionIcon">
+ <iron-icon icon="gr-icons:description">
+ </iron-icon>
+ </div>
+ <div class="sectionContent">
+ Test Description
+ </div>
+ </div>
+ <div class="button">
+ <gr-button
+ aria-disabled="false"
+ id="toggleConditionsButton"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ View conditions
+ <iron-icon icon="gr-icons:expand-more">
+ </iron-icon>
+ </gr-button>
+ </div>
+ </div>
+ `);
+ });
+
+ suite('quick approve label', () => {
+ const submitRequirement: SubmitRequirementResultInfo = {
+ ...createSubmitRequirementResultInfo(),
+ description: 'Test Description',
+ submittability_expression_result: {
+ ...createSubmitRequirementExpressionInfo(),
+ expression: 'label:Verified=MAX -label:Verified=MIN',
+ },
+ };
+ const account = createAccountWithId();
+ const change: ParsedChangeInfo = {
+ ...createParsedChange(),
+ status: ChangeStatus.NEW,
+ labels: {
+ Verified: {
+ ...createDetailedLabelInfo(),
+ all: [
+ {
+ ...createApproval(),
+ _account_id: account._account_id,
+ permitted_voting_range: {
+ min: -2,
+ max: 2,
+ },
+ },
+ ],
+ },
+ },
+ };
+ test('renders', async () => {
+ const element = await fixture<GrSubmitRequirementHovercard>(
+ html`<gr-submit-requirement-hovercard
+ .requirement=${submitRequirement}
+ .change=${change}
+ .account=${account}
+ ></gr-submit-requirement-hovercard>`
+ );
+ const quickApprove = queryAndAssert(element, '.quickApprove');
+ expect(quickApprove).dom.to.equal(`<div class="button quickApprove">
+ <gr-button
+ aria-disabled="false"
+ link=""
+ role="button"
+ tabindex="0"
+ >Vote Verified +2
+ </gr-button>
+ </div>
+ `);
+ });
+
+ test("doesn't render when already voted max vote", async () => {
+ const changeWithVote = {
+ ...change,
+ labels: {
+ ...change.labels,
+ Verified: {
+ ...createDetailedLabelInfo(),
+ all: [
+ {
+ ...createApproval(),
+ _account_id: account._account_id,
+ permitted_voting_range: {
+ min: -2,
+ max: 2,
+ },
+ value: 2,
+ },
+ ],
+ },
+ },
+ };
+ const element = await fixture<GrSubmitRequirementHovercard>(
+ html`<gr-submit-requirement-hovercard
+ .requirement=${submitRequirement}
+ .change=${changeWithVote}
+ .account=${account}
+ ></gr-submit-requirement-hovercard>`
+ );
+ assert.isUndefined(query(element, '.quickApprove'));
+ });
+
+ test('override button renders', async () => {
+ const submitRequirement: SubmitRequirementResultInfo = {
+ ...createSubmitRequirementResultInfo(),
+ description: 'Test Description',
+ submittability_expression_result: {
+ ...createSubmitRequirementExpressionInfo(),
+ expression: 'label:Verified=MAX -label:Verified=MIN',
+ },
+ override_expression_result: {
+ ...createSubmitRequirementExpressionInfo(),
+ expression: 'label:Build-Cop=MAX',
+ },
+ };
+ const account = createAccountWithId();
+ const change: ParsedChangeInfo = {
+ ...createParsedChange(),
+ status: ChangeStatus.NEW,
+ labels: {
+ 'Build-Cop': {
+ ...createDetailedLabelInfo(),
+ all: [
+ {
+ ...createApproval(),
+ _account_id: account._account_id,
+ permitted_voting_range: {
+ min: -2,
+ max: 2,
+ },
+ },
+ ],
+ },
+ },
+ };
+ const element = await fixture<GrSubmitRequirementHovercard>(
+ html`<gr-submit-requirement-hovercard
+ .requirement=${submitRequirement}
+ .change=${change}
+ .account=${account}
+ ></gr-submit-requirement-hovercard>`
+ );
+ const quickApprove = queryAndAssert(element, '.quickApprove');
+ expect(quickApprove).dom.to.equal(`<div class="button quickApprove">
+ <gr-button
+ aria-disabled="false"
+ link=""
+ role="button"
+ tabindex="0"
+ >Override (Build-Cop)
+ </gr-button>
+ </div>
+ `);
+ });
+ });
+});
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index e8859fd..4294e3f 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -17,7 +17,9 @@
import '../../shared/gr-label-info/gr-label-info';
import '../gr-submit-requirement-hovercard/gr-submit-requirement-hovercard';
import '../gr-trigger-vote-hovercard/gr-trigger-vote-hovercard';
-import {LitElement, css, html} from 'lit';
+import '../gr-change-summary/gr-change-summary';
+import '../../shared/gr-limited-text/gr-limited-text';
+import {LitElement, css, html, TemplateResult} from 'lit';
import {customElement, property, state} from 'lit/decorators';
import {ParsedChangeInfo} from '../../../types/types';
import {
@@ -29,26 +31,31 @@
SubmitRequirementResultInfo,
SubmitRequirementStatus,
} from '../../../api/rest-api';
-import {unique} from '../../../utils/common-util';
import {
extractAssociatedLabels,
getAllUniqueApprovals,
+ getRequirements,
+ getTriggerVotes,
hasNeutralStatus,
hasVotes,
iconForStatus,
orderSubmitRequirements,
} from '../../../utils/label-util';
import {fontStyles} from '../../../styles/gr-font-styles';
-import {charsOnly, pluralize} from '../../../utils/string-util';
+import {charsOnly} from '../../../utils/string-util';
import {subscribe} from '../../lit/subscription-controller';
-import {
- allRunsLatestPatchsetLatestAttempt$,
- CheckRun,
-} from '../../../services/checks/checks-model';
+import {CheckRun} from '../../../services/checks/checks-model';
import {getResultsOf, hasResultsOf} from '../../../services/checks/checks-util';
import {Category} from '../../../api/checks';
import '../../shared/gr-vote-chip/gr-vote-chip';
+import {fireShowPrimaryTab} from '../../../utils/event-util';
+import {PrimaryTab} from '../../../constants/constants';
+import {getAppContext} from '../../../services/app-context';
+import {submitRequirementsStyles} from '../../../styles/gr-submit-requirements-styles';
+/**
+ * @attr {Boolean} suppress-title - hide titles, currently for hovercard view
+ */
@customElement('gr-submit-requirements')
export class GrSubmitRequirements extends LitElement {
@property({type: Object})
@@ -60,31 +67,33 @@
@property({type: Boolean})
mutable?: boolean;
+ @property({type: Boolean, attribute: 'disable-hovercards'})
+ disableHovercards = false;
+
+ @property({type: Boolean, attribute: 'disable-endpoints'})
+ disableEndpoints = false;
+
@state()
runs: CheckRun[] = [];
static override get styles() {
return [
fontStyles,
+ submitRequirementsStyles,
css`
+ :host([suppress-title]) .metadata-title {
+ display: none;
+ }
.metadata-title {
color: var(--deemphasized-text-color);
padding-left: var(--metadata-horizontal-padding);
margin: 0 0 var(--spacing-s);
- border-top: 1px solid var(--border-color);
padding-top: var(--spacing-s);
}
iron-icon {
width: var(--line-height-normal, 20px);
height: var(--line-height-normal, 20px);
}
- iron-icon.check,
- iron-icon.overridden {
- color: var(--success-foreground);
- }
- iron-icon.close {
- color: var(--error-foreground);
- }
.requirements,
section.trigger-votes {
margin-left: var(--spacing-l);
@@ -108,6 +117,7 @@
}
td {
padding: var(--spacing-s);
+ white-space: nowrap;
}
.votes-cell {
display: flex;
@@ -122,28 +132,29 @@
gr-vote-chip {
margin-right: var(--spacing-s);
}
+ gr-checks-chip {
+ /* .checksChip has top: 2px, this is canceling it */
+ margin-top: -2px;
+ }
`,
];
}
+ private readonly checksModel = getAppContext().checksModel;
+
constructor() {
super();
- subscribe(this, allRunsLatestPatchsetLatestAttempt$, x => (this.runs = x));
+ subscribe(
+ this,
+ this.checksModel.allRunsLatestPatchsetLatestAttempt$,
+ x => (this.runs = x)
+ );
}
override render() {
- let submit_requirements = orderSubmitRequirements(
- this.change?.submit_requirements ?? []
- ).filter(req => req.status !== SubmitRequirementStatus.NOT_APPLICABLE);
-
- const hasNonLegacyRequirements = submit_requirements.some(
- req => req.is_legacy === false
+ const submit_requirements = orderSubmitRequirements(
+ getRequirements(this.change)
);
- if (hasNonLegacyRequirements) {
- submit_requirements = submit_requirements.filter(
- req => req.is_legacy === false
- );
- }
return html` <h3
class="metadata-title heading-3"
@@ -160,40 +171,70 @@
</tr>
</thead>
<tbody>
- ${submit_requirements.map(
- requirement => html`<tr
- id="requirement-${charsOnly(requirement.name)}"
- >
- <td>${this.renderStatus(requirement.status)}</td>
- <td class="name">
- <gr-limited-text
- class="name"
- limit="25"
- .text="${requirement.name}"
- ></gr-limited-text>
- </td>
- <td>
- <div class="votes-cell">
- ${this.renderVotes(requirement)}
- ${this.renderChecks(requirement)}
- </div>
- </td>
- </tr>`
+ ${submit_requirements.map(requirement =>
+ this.renderRequirement(requirement)
)}
</tbody>
</table>
- ${submit_requirements.map(
- requirement => html`
- <gr-submit-requirement-hovercard
- for="requirement-${charsOnly(requirement.name)}"
- .requirement="${requirement}"
- .change="${this.change}"
- .account="${this.account}"
- .mutable="${this.mutable ?? false}"
- ></gr-submit-requirement-hovercard>
- `
- )}
- ${this.renderTriggerVotes(submit_requirements)}`;
+ ${this.disableHovercards
+ ? ''
+ : submit_requirements.map(
+ requirement => html`
+ <gr-submit-requirement-hovercard
+ for="requirement-${charsOnly(requirement.name)}"
+ .requirement="${requirement}"
+ .change="${this.change}"
+ .account="${this.account}"
+ .mutable="${this.mutable ?? false}"
+ ></gr-submit-requirement-hovercard>
+ `
+ )}
+ ${this.renderTriggerVotes()}`;
+ }
+
+ renderRequirement(requirement: SubmitRequirementResultInfo) {
+ return html`
+ <tr id="requirement-${charsOnly(requirement.name)}">
+ <td>${this.renderStatus(requirement.status)}</td>
+ <td class="name">
+ <gr-limited-text
+ class="name"
+ limit="25"
+ .text="${requirement.name}"
+ ></gr-limited-text>
+ </td>
+ <td>
+ ${this.renderEndpoint(
+ requirement,
+ html`${this.renderVotesAndChecksChips(requirement)}
+ ${this.renderOverrideLabels(requirement)}`
+ )}
+ </td>
+ </tr>
+ `;
+ }
+
+ renderEndpoint(
+ requirement: SubmitRequirementResultInfo,
+ slot: TemplateResult
+ ) {
+ if (this.disableEndpoints) return slot;
+
+ const endpointName = this.calculateEndpointName(requirement.name);
+ return html`<gr-endpoint-decorator
+ class="votes-cell"
+ name="${endpointName}"
+ >
+ <gr-endpoint-param
+ name="change"
+ .value=${this.change}
+ ></gr-endpoint-param>
+ <gr-endpoint-param
+ name="requirement"
+ .value=${requirement}
+ ></gr-endpoint-param>
+ ${slot}
+ </gr-endpoint-decorator>`;
}
renderStatus(status: SubmitRequirementStatus) {
@@ -206,7 +247,10 @@
></iron-icon>`;
}
- renderVotes(requirement: SubmitRequirementResultInfo) {
+ renderVotesAndChecksChips(requirement: SubmitRequirementResultInfo) {
+ if (requirement.status === SubmitRequirementStatus.ERROR) {
+ return html`<span class="error">Error</span>`;
+ }
const requirementLabels = extractAssociatedLabels(requirement);
const allLabels = this.change?.labels ?? {};
const associatedLabels = Object.keys(allLabels).filter(label =>
@@ -216,11 +260,17 @@
const everyAssociatedLabelsIsWithoutVotes = associatedLabels.every(
label => !hasVotes(allLabels[label])
);
- if (everyAssociatedLabelsIsWithoutVotes) return html`No votes`;
- return associatedLabels.map(label =>
+ const checksChips = this.renderChecks(requirement);
+
+ if (everyAssociatedLabelsIsWithoutVotes) {
+ return checksChips || html`No votes`;
+ }
+
+ return html`${associatedLabels.map(label =>
this.renderLabelVote(label, allLabels)
- );
+ )}
+ ${checksChips}`;
}
renderLabelVote(label: string, labels: LabelNameToInfoMap) {
@@ -257,26 +307,44 @@
(sum, run) => sum + getResultsOf(run, Category.ERROR).length,
0
);
- if (runsCount > 0) {
- return html`<span class="check-error"
- ><iron-icon icon="gr-icons:error"></iron-icon>${pluralize(
- runsCount,
- 'error'
- )}</span
- >`;
+ if (runsCount === 0) return;
+ const links = [];
+ if (requirementRuns.length === 1 && requirementRuns[0].statusLink) {
+ links.push(requirementRuns[0].statusLink);
}
- return;
+ return html`<gr-checks-chip
+ .text=${`${runsCount}`}
+ .links=${links}
+ .statusOrCategory=${Category.ERROR}
+ @click="${() => {
+ fireShowPrimaryTab(this, PrimaryTab.CHECKS, false, {
+ checksTab: {
+ statusOrCategory: Category.ERROR,
+ },
+ });
+ }}"
+ ></gr-checks-chip>`;
}
- renderTriggerVotes(submitReqs: SubmitRequirementResultInfo[]) {
+ renderOverrideLabels(requirement: SubmitRequirementResultInfo) {
+ if (requirement.status !== SubmitRequirementStatus.OVERRIDDEN) return;
+ const requirementLabels = extractAssociatedLabels(
+ requirement,
+ 'onlyOverride'
+ ).filter(label => {
+ const allLabels = this.change?.labels ?? {};
+ return allLabels[label] && hasVotes(allLabels[label]);
+ });
+ return requirementLabels.map(
+ label => html`<span class="overrideLabel">${label}</span>`
+ );
+ }
+
+ renderTriggerVotes() {
const labels = this.change?.labels ?? {};
- const allLabels = Object.keys(labels);
- const labelAssociatedWithSubmitReqs = submitReqs
- .flatMap(req => extractAssociatedLabels(req))
- .filter(unique);
- const triggerVotes = allLabels
- .filter(label => !labelAssociatedWithSubmitReqs.includes(label))
- .filter(label => hasVotes(labels[label]));
+ const triggerVotes = getTriggerVotes(this.change).filter(label =>
+ hasVotes(labels[label])
+ );
if (!triggerVotes.length) return;
return html`<h3 class="metadata-title heading-3">Trigger Votes</h3>
<section class="trigger-votes">
@@ -292,6 +360,14 @@
)}
</section>`;
}
+
+ // not private for tests
+ calculateEndpointName(requirementName: string) {
+ // remove class name annnotation after ~
+ const name = requirementName.split('~')[0];
+ const normalizedName = charsOnly(name).toLowerCase();
+ return `submit-requirement-${normalizedName}`;
+ }
}
@customElement('gr-trigger-vote')
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
new file mode 100644
index 0000000..5a094ef
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements_test.ts
@@ -0,0 +1,125 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import {fixture} from '@open-wc/testing-helpers';
+import {html} from 'lit';
+import './gr-submit-requirements';
+import {GrSubmitRequirements} from './gr-submit-requirements';
+import {
+ createAccountWithIdNameAndEmail,
+ createApproval,
+ createDetailedLabelInfo,
+ createParsedChange,
+ createSubmitRequirementExpressionInfo,
+ createSubmitRequirementResultInfo,
+} from '../../../test/test-data-generators';
+import {SubmitRequirementResultInfo} from '../../../api/rest-api';
+import {ParsedChangeInfo} from '../../../types/types';
+
+suite('gr-submit-requirements tests', () => {
+ let element: GrSubmitRequirements;
+ setup(async () => {
+ const submitRequirement: SubmitRequirementResultInfo = {
+ ...createSubmitRequirementResultInfo(),
+ description: 'Test Description',
+ submittability_expression_result: {
+ ...createSubmitRequirementExpressionInfo(),
+ expression: 'label:Verified=MAX -label:Verified=MIN',
+ },
+ };
+ const change: ParsedChangeInfo = {
+ ...createParsedChange(),
+ submit_requirements: [submitRequirement],
+ labels: {
+ Verified: {
+ ...createDetailedLabelInfo(),
+ all: [
+ {
+ ...createApproval(),
+ value: 2,
+ },
+ ],
+ },
+ },
+ };
+ const account = createAccountWithIdNameAndEmail();
+ element = await fixture<GrSubmitRequirements>(
+ html`<gr-submit-requirements
+ .change=${change}
+ .account=${account}
+ ></gr-submit-requirements>`
+ );
+ });
+
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(`<h3
+ class="heading-3 metadata-title"
+ id="submit-requirements-caption"
+ >
+ Submit Requirements
+ </h3>
+ <table
+ aria-labelledby="submit-requirements-caption"
+ class="requirements"
+ >
+ <thead hidden="">
+ <tr>
+ <th>Status</th>
+ <th>Name</th>
+ <th>Votes</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr id="requirement-Verified">
+ <td>
+ <iron-icon
+ aria-label="satisfied"
+ class="check-circle-filled"
+ icon="gr-icons:check-circle-filled"
+ role="img"
+ >
+ </iron-icon>
+ </td>
+ <td class="name">
+ <gr-limited-text class="name" limit="25"></gr-limited-text>
+ </td>
+ <td>
+ <gr-endpoint-decorator
+ class="votes-cell"
+ name="submit-requirement-verified"
+ >
+ <gr-endpoint-param name="change"></gr-endpoint-param>
+ <gr-endpoint-param name="requirement"></gr-endpoint-param>
+ <gr-vote-chip></gr-vote-chip>
+ </gr-endpoint-decorator>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <gr-submit-requirement-hovercard for="requirement-Verified">
+ </gr-submit-requirement-hovercard>
+ `);
+ });
+
+ test('calculateEndpointName()', () => {
+ assert.equal(
+ element.calculateEndpointName('code-owners~CodeOwnerSub'),
+ 'submit-requirement-codeowners'
+ );
+ });
+});
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 deea4ab..490162b 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
@@ -17,50 +17,36 @@
import '../../../styles/shared-styles';
import '../../shared/gr-comment-thread/gr-comment-thread';
import '../../shared/gr-dropdown-list/gr-dropdown-list';
-
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-thread-list_html';
-import {parseDate} from '../../../utils/date-util';
-
-import {CommentSide, SpecialFilePath} from '../../../constants/constants';
-import {computed, customElement, observe, property} from '@polymer/decorators';
-import {
- PolymerSpliceChange,
- PolymerDeepPropertyChange,
-} from '@polymer/polymer/interfaces';
+import {SpecialFilePath} from '../../../constants/constants';
import {
AccountDetailInfo,
AccountInfo,
- ChangeInfo,
NumericChangeId,
UrlEncodedCommentId,
} from '../../../types/common';
+import {ChangeMessageId} from '../../../api/rest-api';
import {
CommentThread,
- isDraft,
- isUnresolved,
+ getCommentAuthors,
+ hasHumanReply,
isDraftThread,
isRobotThread,
- hasHumanReply,
- getCommentAuthors,
- computeId,
- UIComment,
+ isUnresolved,
+ lastUpdated,
} from '../../../utils/comment-util';
import {pluralize} from '../../../utils/string-util';
-import {assertIsDefined, assertNever} from '../../../utils/common-util';
+import {assertIsDefined} from '../../../utils/common-util';
import {CommentTabState} from '../../../types/events';
import {DropdownItem} from '../../shared/gr-dropdown-list/gr-dropdown-list';
import {GrAccountChip} from '../../shared/gr-account-chip/gr-account-chip';
-
-interface CommentThreadWithInfo {
- thread: CommentThread;
- hasRobotComment: boolean;
- hasHumanReplyToRobotComment: boolean;
- unresolved: boolean;
- isEditing: boolean;
- hasDraft: boolean;
- updated?: Date;
-}
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property, queryAll, state} from 'lit/decorators';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {subscribe} from '../../lit/subscription-controller';
+import {ParsedChangeInfo} from '../../../types/types';
+import {repeat} from 'lit/directives/repeat';
+import {GrCommentThread} from '../../shared/gr-comment-thread/gr-comment-thread';
+import {getAppContext} from '../../../services/app-context';
enum SortDropdownState {
TIMESTAMP = 'Latest timestamp',
@@ -69,571 +55,516 @@
export const __testOnly_SortDropdownState = SortDropdownState;
-@customElement('gr-thread-list')
-export class GrThreadList extends PolymerElement {
- static get template() {
- return htmlTemplate;
+/**
+ * Order as follows:
+ * - Patchset level threads (descending based on patchset number)
+ * - unresolved
+ * - comments with drafts
+ * - comments without drafts
+ * - resolved
+ * - comments with drafts
+ * - comments without drafts
+ * - File name
+ * - Line number
+ * - Unresolved (descending based on patchset number)
+ * - comments with drafts
+ * - comments without drafts
+ * - Resolved (descending based on patchset number)
+ * - comments with drafts
+ * - comments without drafts
+ */
+export function compareThreads(
+ c1: CommentThread,
+ c2: CommentThread,
+ byTimestamp = false
+) {
+ if (byTimestamp) {
+ const c1Time = lastUpdated(c1)?.getTime() ?? 0;
+ const c2Time = lastUpdated(c2)?.getTime() ?? 0;
+ const timeDiff = c2Time - c1Time;
+ if (timeDiff !== 0) return c2Time - c1Time;
}
- @property({type: Object})
- change?: ChangeInfo;
+ if (c1.path !== c2.path) {
+ // '/PATCHSET' will not come before '/COMMIT' when sorting
+ // alphabetically so move it to the front explicitly
+ if (c1.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
+ return -1;
+ }
+ if (c2.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
+ return 1;
+ }
+ return c1.path.localeCompare(c2.path);
+ }
+ // Convert 'FILE' and 'LOST' to undefined.
+ const line1 = typeof c1.line === 'number' ? c1.line : undefined;
+ const line2 = typeof c2.line === 'number' ? c2.line : undefined;
+ if (line1 !== line2) {
+ // one of them is a FILE/LOST comment, show first
+ if (line1 === undefined) return -1;
+ if (line2 === undefined) return 1;
+ // Lower line numbers first.
+ return line1 < line2 ? -1 : 1;
+ }
+
+ if (c1.patchNum !== c2.patchNum) {
+ // `patchNum` should be required, but show undefined first.
+ if (c1.patchNum === undefined) return -1;
+ if (c2.patchNum === undefined) return 1;
+ // Higher patchset numbers first.
+ return c1.patchNum > c2.patchNum ? -1 : 1;
+ }
+
+ // Sorting should not be based on the thread being unresolved or being a draft
+ // thread, because that would be a surprising re-sort when the thread changes
+ // state.
+
+ const c1Time = lastUpdated(c1)?.getTime() ?? 0;
+ const c2Time = lastUpdated(c2)?.getTime() ?? 0;
+ if (c2Time !== c1Time) {
+ // Newer comments first.
+ return c2Time - c1Time;
+ }
+
+ return 0;
+}
+
+@customElement('gr-thread-list')
+export class GrThreadList extends LitElement {
+ @queryAll('gr-comment-thread')
+ threadElements?: NodeList;
+
+ /**
+ * Raw list of threads for the component to show.
+ *
+ * ATTENTION! this.threads should never be used directly within the component.
+ *
+ * Either use getAllThreads(), which applies filters that are inherent to what
+ * the component is supposed to render,
+ * e.g. onlyShowRobotCommentsWithHumanReply.
+ *
+ * Or use getDisplayedThreads(), which applies the currently selected filters
+ * on top.
+ */
@property({type: Array})
threads: CommentThread[] = [];
- @property({type: String})
- changeNum?: NumericChangeId;
-
- @property({type: Boolean})
- loggedIn?: boolean;
-
- @property({type: Array})
- _sortedThreads: CommentThread[] = [];
-
- @property({type: Boolean})
+ @property({type: Boolean, attribute: 'show-comment-context'})
showCommentContext = false;
- @property({
- computed:
- '_computeDisplayedThreads(_sortedThreads.*, unresolvedOnly, ' +
- '_draftsOnly, onlyShowRobotCommentsWithHumanReply, selectedAuthors)',
- type: Array,
- })
- _displayedThreads: CommentThread[] = [];
-
- // thread-list is used in multiple places like the change log, hence
- // keeping the default to be false. When used in comments tab, it's
- // set as true.
- @property({type: Boolean})
+ /** Along with `draftsOnly` is the currently selected filter. */
+ @property({type: Boolean, attribute: 'unresolved-only'})
unresolvedOnly = false;
- @property({type: Boolean})
- _draftsOnly = false;
-
- @property({type: Boolean})
+ @property({
+ type: Boolean,
+ attribute: 'only-show-robot-comments-with-human-reply',
+ })
onlyShowRobotCommentsWithHumanReply = false;
- @property({type: Boolean})
+ @property({type: Boolean, attribute: 'hide-dropdown'})
hideDropdown = false;
- @property({type: Object, observer: '_commentTabStateChange'})
+ @property({type: Object, attribute: 'comment-tab-state'})
commentTabState?: CommentTabState;
- @property({type: Object})
- sortDropdownValue: SortDropdownState = SortDropdownState.TIMESTAMP;
-
- @property({type: Array, notify: true})
- selectedAuthors: AccountInfo[] = [];
-
- @property({type: Object})
- account?: AccountDetailInfo;
-
- @computed('unresolvedOnly', '_draftsOnly')
- get commentsDropdownValue() {
- // set initial value and triggered when comment summary chips are clicked
- if (this._draftsOnly) return CommentTabState.DRAFTS;
- return this.unresolvedOnly
- ? CommentTabState.UNRESOLVED
- : CommentTabState.SHOW_ALL;
- }
-
- @property({type: String})
+ @property({type: String, attribute: 'scroll-comment-id'})
scrollCommentId?: UrlEncodedCommentId;
- _showEmptyThreadsMessage(
- threads: CommentThread[],
- displayedThreads: CommentThread[],
- unresolvedOnly: boolean
- ) {
- if (!threads || !displayedThreads) return false;
- return !threads.length || (unresolvedOnly && !displayedThreads.length);
+ /**
+ * Optional context information when threads are being displayed for a
+ * specific change message. That influences which comments are expanded or
+ * collapsed by default.
+ */
+ @property({type: String, attribute: 'message-id'})
+ messageId?: ChangeMessageId;
+
+ @state()
+ changeNum?: NumericChangeId;
+
+ @state()
+ change?: ParsedChangeInfo;
+
+ @state()
+ account?: AccountDetailInfo;
+
+ @state()
+ selectedAuthors: AccountInfo[] = [];
+
+ @state()
+ sortDropdownValue: SortDropdownState = SortDropdownState.TIMESTAMP;
+
+ /** Along with `unresolvedOnly` is the currently selected filter. */
+ @state()
+ draftsOnly = false;
+
+ private readonly changeModel = getAppContext().changeModel;
+
+ private readonly userModel = getAppContext().userModel;
+
+ constructor() {
+ super();
+ subscribe(this, this.changeModel.changeNum$, x => (this.changeNum = x));
+ subscribe(this, this.changeModel.change$, x => (this.change = x));
+ subscribe(this, this.userModel.account$, x => (this.account = x));
}
- _computeEmptyThreadsMessage(threads: CommentThread[]) {
- return !threads.length ? 'No comments' : 'No unresolved comments';
+ override willUpdate(changed: PropertyValues) {
+ if (changed.has('commentTabState')) this.onCommentTabStateUpdate();
+ if (changed.has('scrollCommentId')) this.onScrollCommentIdUpdate();
}
- _showPartyPopper(threads: CommentThread[]) {
- return !!threads.length;
- }
-
- _computeResolvedCommentsMessage(
- threads: CommentThread[],
- displayedThreads: CommentThread[],
- unresolvedOnly: boolean,
- onlyShowRobotCommentsWithHumanReply: boolean
- ) {
- if (onlyShowRobotCommentsWithHumanReply) {
- threads = this.filterRobotThreadsWithoutHumanReply(threads) ?? [];
+ private onCommentTabStateUpdate() {
+ switch (this.commentTabState) {
+ case CommentTabState.UNRESOLVED:
+ this.handleOnlyUnresolved();
+ break;
+ case CommentTabState.DRAFTS:
+ this.handleOnlyDrafts();
+ break;
+ case CommentTabState.SHOW_ALL:
+ this.handleAllComments();
+ break;
}
- if (unresolvedOnly && threads.length && !displayedThreads.length) {
- return `Show ${pluralize(threads.length, 'resolved comment')}`;
- }
- return '';
}
- _showResolvedCommentsButton(
- threads: CommentThread[],
- displayedThreads: CommentThread[],
- unresolvedOnly: boolean
- ) {
- return unresolvedOnly && threads.length && !displayedThreads.length;
+ /**
+ * When user wants to scroll to a comment, render all comments so that the
+ * appropriate comment can be scrolled into view.
+ */
+ private onScrollCommentIdUpdate() {
+ if (this.scrollCommentId) this.handleAllComments();
}
- _handleResolvedCommentsMessageClick() {
- this.unresolvedOnly = !this.unresolvedOnly;
+ static override get styles() {
+ return [
+ sharedStyles,
+ css`
+ #threads {
+ display: block;
+ }
+ gr-comment-thread {
+ display: block;
+ margin-bottom: var(--spacing-m);
+ }
+ .header {
+ align-items: center;
+ background-color: var(--background-color-primary);
+ border-bottom: 1px solid var(--border-color);
+ border-top: 1px solid var(--border-color);
+ display: flex;
+ justify-content: left;
+ padding: var(--spacing-s) var(--spacing-l);
+ }
+ .draftsOnly:not(.unresolvedOnly) gr-comment-thread[has-draft],
+ .unresolvedOnly:not(.draftsOnly) gr-comment-thread[unresolved],
+ .draftsOnly.unresolvedOnly gr-comment-thread[has-draft][unresolved] {
+ display: block;
+ }
+ .thread-separator {
+ border-top: 1px solid var(--border-color);
+ margin-top: var(--spacing-xl);
+ }
+ .show-resolved-comments {
+ box-shadow: none;
+ padding-left: var(--spacing-m);
+ }
+ .partypopper {
+ margin-right: var(--spacing-s);
+ }
+ gr-dropdown-list {
+ --trigger-style-text-color: var(--primary-text-color);
+ --trigger-style-font-family: var(--font-family);
+ }
+ .filter-text,
+ .sort-text,
+ .author-text {
+ margin-right: var(--spacing-s);
+ color: var(--deemphasized-text-color);
+ }
+ .author-text {
+ margin-left: var(--spacing-m);
+ }
+ gr-account-label {
+ --account-max-length: 120px;
+ display: inline-block;
+ user-select: none;
+ --label-border-radius: 8px;
+ margin: 0 var(--spacing-xs);
+ padding: var(--spacing-xs) var(--spacing-m);
+ line-height: var(--line-height-normal);
+ cursor: pointer;
+ }
+ gr-account-label:focus {
+ outline: none;
+ }
+ gr-account-label:hover,
+ gr-account-label:hover {
+ box-shadow: var(--elevation-level-1);
+ cursor: pointer;
+ }
+ `,
+ ];
}
- getSortDropdownEntires() {
+ override render() {
+ return html`
+ ${this.renderDropdown()}
+ <div id="threads" part="threads">
+ ${this.renderEmptyThreadsMessage()} ${this.renderCommentThreads()}
+ </div>
+ `;
+ }
+
+ private renderDropdown() {
+ if (this.hideDropdown) return;
+ return html`
+ <div class="header">
+ <span class="sort-text">Sort By:</span>
+ <gr-dropdown-list
+ id="sortDropdown"
+ .value="${this.sortDropdownValue}"
+ @value-change="${(e: CustomEvent) =>
+ (this.sortDropdownValue = e.detail.value)}"
+ .items="${this.getSortDropdownEntries()}"
+ >
+ </gr-dropdown-list>
+ <span class="separator"></span>
+ <span class="filter-text">Filter By:</span>
+ <gr-dropdown-list
+ id="filterDropdown"
+ .value="${this.getCommentsDropdownValue()}"
+ @value-change="${this.handleCommentsDropdownValueChange}"
+ .items="${this.getCommentsDropdownEntries()}"
+ >
+ </gr-dropdown-list>
+ ${this.renderAuthorChips()}
+ </div>
+ `;
+ }
+
+ private renderEmptyThreadsMessage() {
+ const threads = this.getAllThreads();
+ const threadsEmpty = threads.length === 0;
+ const displayedEmpty = this.getDisplayedThreads().length === 0;
+ if (!displayedEmpty) return;
+ const showPopper = this.unresolvedOnly && !threadsEmpty;
+ const popper = html`<span class="partypopper">🎉</span>`;
+ const showButton = this.unresolvedOnly && !threadsEmpty;
+ const button = html`
+ <gr-button
+ class="show-resolved-comments"
+ link
+ @click="${this.handleAllComments}"
+ >Show ${pluralize(threads.length, 'resolved comment')}</gr-button
+ >
+ `;
+ return html`
+ <div>
+ <span>
+ ${showPopper ? popper : undefined}
+ ${threadsEmpty ? 'No comments' : 'No unresolved comments'}
+ ${showButton ? button : undefined}
+ </span>
+ </div>
+ `;
+ }
+
+ private renderCommentThreads() {
+ const threads = this.getDisplayedThreads();
+ return repeat(
+ threads,
+ thread => thread.rootId,
+ (thread, index) => {
+ const isFirst =
+ index === 0 || threads[index - 1].path !== threads[index].path;
+ const separator =
+ index !== 0 && isFirst
+ ? html`<div class="thread-separator"></div>`
+ : undefined;
+ const commentThread = this.renderCommentThread(thread, isFirst);
+ return html`${separator}${commentThread}`;
+ }
+ );
+ }
+
+ private renderCommentThread(thread: CommentThread, isFirst: boolean) {
+ return html`
+ <gr-comment-thread
+ .thread="${thread}"
+ show-file-path
+ ?show-ported-comment="${thread.ported}"
+ ?show-comment-context="${this.showCommentContext}"
+ ?show-file-name="${isFirst}"
+ .messageId="${this.messageId}"
+ ?should-scroll-into-view="${thread.rootId === this.scrollCommentId}"
+ @comment-thread-editing-changed="${() => {
+ this.requestUpdate();
+ }}"
+ ></gr-comment-thread>
+ `;
+ }
+
+ private renderAuthorChips() {
+ const authors = getCommentAuthors(this.getDisplayedThreads(), this.account);
+ if (authors.length === 0) return;
+ return html`<span class="author-text">From:</span>${authors.map(author =>
+ this.renderAccountChip(author)
+ )}`;
+ }
+
+ private renderAccountChip(account: AccountInfo) {
+ const selected = this.selectedAuthors.some(
+ a => a._account_id === account._account_id
+ );
+ return html`
+ <gr-account-label
+ .account="${account}"
+ @click="${this.handleAccountClicked}"
+ selectionChipStyle
+ ?selected="${selected}"
+ ></gr-account-label>
+ `;
+ }
+
+ private getCommentsDropdownValue() {
+ if (this.draftsOnly) return CommentTabState.DRAFTS;
+ if (this.unresolvedOnly) return CommentTabState.UNRESOLVED;
+ return CommentTabState.SHOW_ALL;
+ }
+
+ private getSortDropdownEntries() {
return [
{text: SortDropdownState.FILES, value: SortDropdownState.FILES},
{text: SortDropdownState.TIMESTAMP, value: SortDropdownState.TIMESTAMP},
];
}
- getCommentsDropdownEntires(threads: CommentThread[], loggedIn?: boolean) {
- const items: DropdownItem[] = [
- {
- text: `Unresolved (${this._countUnresolved(threads)})`,
- value: CommentTabState.UNRESOLVED,
- },
- {
- text: `All (${this._countAllThreads(threads)})`,
- value: CommentTabState.SHOW_ALL,
- },
- ];
- if (loggedIn)
- items.splice(1, 0, {
- text: `Drafts (${this._countDrafts(threads)})`,
+ // private, but visible for testing
+ getCommentsDropdownEntries() {
+ const items: DropdownItem[] = [];
+ const threads = this.getAllThreads();
+ items.push({
+ text: `Unresolved (${threads.filter(isUnresolved).length})`,
+ value: CommentTabState.UNRESOLVED,
+ });
+ if (this.account) {
+ items.push({
+ text: `Drafts (${threads.filter(isDraftThread).length})`,
value: CommentTabState.DRAFTS,
});
+ }
+ items.push({
+ text: `All (${threads.length})`,
+ value: CommentTabState.SHOW_ALL,
+ });
return items;
}
- getCommentAuthors(threads?: CommentThread[], account?: AccountDetailInfo) {
- return getCommentAuthors(threads, account);
- }
-
- handleAccountClicked(e: MouseEvent) {
+ private handleAccountClicked(e: MouseEvent) {
const account = (e.target as GrAccountChip).account;
assertIsDefined(account, 'account');
- const index = this.selectedAuthors.findIndex(
- author => author._account_id === account._account_id
- );
- if (index === -1) this.push('selectedAuthors', account);
- else this.splice('selectedAuthors', index, 1);
- // re-assign so that isSelected template method is called
- this.selectedAuthors = [...this.selectedAuthors];
+ const predicate = (a: AccountInfo) => a._account_id === account._account_id;
+ const found = this.selectedAuthors.find(predicate);
+ if (found) {
+ this.selectedAuthors = this.selectedAuthors.filter(a => !predicate(a));
+ } else {
+ this.selectedAuthors = [...this.selectedAuthors, account];
+ }
}
- isSelected(author: AccountInfo, selectedAuthors: AccountInfo[]) {
- return selectedAuthors.some(a => a._account_id === author._account_id);
- }
-
- computeShouldScrollIntoView(
- comments: UIComment[],
- scrollCommentId?: UrlEncodedCommentId
- ) {
- const comment = comments?.[0];
- if (!comment) return false;
- return computeId(comment) === scrollCommentId;
- }
-
- handleSortDropdownValueChange(e: CustomEvent) {
- this.sortDropdownValue = e.detail.value;
- /*
- * Ideally we would have updateSortedThreads observe on sortDropdownValue
- * but the method triggered re-render only when the length of threads
- * changes, hence keep the explicit resortThreads method
- */
- this.resortThreads(this.threads);
- }
-
+ // private, but visible for testing
handleCommentsDropdownValueChange(e: CustomEvent) {
const value = e.detail.value;
- if (value === CommentTabState.UNRESOLVED) this._handleOnlyUnresolved();
- else if (value === CommentTabState.DRAFTS) this._handleOnlyDrafts();
- else this._handleAllComments();
- }
-
- _compareThreads(c1: CommentThreadWithInfo, c2: CommentThreadWithInfo) {
- if (
- this.sortDropdownValue === SortDropdownState.TIMESTAMP &&
- !this.hideDropdown
- ) {
- if (c1.updated && c2.updated) return c1.updated > c2.updated ? -1 : 1;
+ switch (value) {
+ case CommentTabState.UNRESOLVED:
+ this.handleOnlyUnresolved();
+ break;
+ case CommentTabState.DRAFTS:
+ this.handleOnlyDrafts();
+ break;
+ default:
+ this.handleAllComments();
}
-
- if (c1.thread.path !== c2.thread.path) {
- // '/PATCHSET' will not come before '/COMMIT' when sorting
- // alphabetically so move it to the front explicitly
- if (c1.thread.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
- return -1;
- }
- if (c2.thread.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
- return 1;
- }
- return c1.thread.path.localeCompare(c2.thread.path);
- }
-
- // Patchset comments have no line/range associated with them
- if (c1.thread.line !== c2.thread.line) {
- if (!c1.thread.line || !c2.thread.line) {
- // one of them is a file level comment, show first
- return c1.thread.line ? 1 : -1;
- }
- return c1.thread.line < c2.thread.line ? -1 : 1;
- }
-
- if (c1.thread.patchNum !== c2.thread.patchNum) {
- if (!c1.thread.patchNum) return 1;
- if (!c2.thread.patchNum) return -1;
- // Threads left on Base when comparing Base vs X have patchNum = X
- // and CommentSide = PARENT
- // Threads left on 'edit' have patchNum set as latestPatchNum
- return c1.thread.patchNum > c2.thread.patchNum ? -1 : 1;
- }
-
- if (c2.unresolved !== c1.unresolved) {
- if (!c1.unresolved) return 1;
- if (!c2.unresolved) return -1;
- }
-
- if (c2.hasDraft !== c1.hasDraft) {
- if (!c1.hasDraft) return 1;
- if (!c2.hasDraft) return -1;
- }
-
- if (c2.updated !== c1.updated) {
- if (!c1.updated) return 1;
- if (!c2.updated) return -1;
- return c2.updated.getTime() - c1.updated.getTime();
- }
-
- if (c2.thread.rootId !== c1.thread.rootId) {
- if (!c1.thread.rootId) return 1;
- if (!c2.thread.rootId) return -1;
- return c1.thread.rootId.localeCompare(c2.thread.rootId);
- }
-
- return 0;
- }
-
- resortThreads(threads: CommentThread[]) {
- const threadsWithInfo = threads.map(thread =>
- this._getThreadWithStatusInfo(thread)
- );
- this._sortedThreads = threadsWithInfo
- .sort((t1, t2) => this._compareThreads(t1, t2))
- .map(threadInfo => threadInfo.thread);
}
/**
- * Observer on threads and update _sortedThreads when needed.
- * Order as follows:
- * - Patchset level threads (descending based on patchset number)
- * - unresolved
- * - comments with drafts
- * - comments without drafts
- * - resolved
- * - comments with drafts
- * - comments without drafts
- * - File name
- * - Line number
- * - Unresolved (descending based on patchset number)
- * - comments with drafts
- * - comments without drafts
- * - Resolved (descending based on patchset number)
- * - comments with drafts
- * - comments without drafts
- *
- * @param threads
- * @param spliceRecord
+ * Returns all threads that the list may show.
*/
- @observe('threads', 'threads.splices')
- _updateSortedThreads(
- threads: CommentThread[],
- _: PolymerSpliceChange<CommentThread[]>
- ) {
- if (!threads || threads.length === 0) {
- this._sortedThreads = [];
- this._displayedThreads = [];
- return;
- }
- // We only want to sort on thread additions / removals to avoid
- // re-rendering on modifications (add new reply / edit draft etc.).
- // https://polymer-library.polymer-project.org/3.0/docs/devguide/observers#array-observation
- // TODO(TS): We have removed a buggy check of the splices here. A splice
- // with addedCount > 0 or removed.length > 0 should also cause re-sorting
- // and re-rendering, but apparently spliceRecord is always undefined for
- // whatever reason.
- // If there is an unsaved draftThread which is supposed to be replaced with
- // a saved draftThread then resort all threads
- const unsavedThread = this._sortedThreads.some(thread =>
- thread.rootId?.includes('draft__')
- );
- if (this._sortedThreads.length === threads.length && !unsavedThread) {
- // Instead of replacing the _sortedThreads which will trigger a re-render,
- // we override all threads inside of it.
- for (const thread of threads) {
- const idxInSortedThreads = this._sortedThreads.findIndex(
- t => t.rootId === thread.rootId
- );
- this.set(`_sortedThreads.${idxInSortedThreads}`, {...thread});
- }
- return;
- }
-
- this.resortThreads(threads);
- }
-
- _computeDisplayedThreads(
- sortedThreadsRecord?: PolymerDeepPropertyChange<
- CommentThread[],
- CommentThread[]
- >,
- unresolvedOnly?: boolean,
- draftsOnly?: boolean,
- onlyShowRobotCommentsWithHumanReply?: boolean,
- selectedAuthors?: AccountInfo[]
- ) {
- if (!sortedThreadsRecord || !sortedThreadsRecord.base) return [];
- return sortedThreadsRecord.base.filter(t =>
- this._shouldShowThread(
- t,
- unresolvedOnly,
- draftsOnly,
- onlyShowRobotCommentsWithHumanReply,
- selectedAuthors
- )
+ // private, but visible for testing
+ getAllThreads() {
+ return this.threads.filter(
+ t =>
+ !this.onlyShowRobotCommentsWithHumanReply ||
+ !isRobotThread(t) ||
+ hasHumanReply(t)
);
}
- _isFirstThreadWithFileName(
- displayedThreads: CommentThread[],
- thread: CommentThread,
- unresolvedOnly?: boolean,
- draftsOnly?: boolean,
- onlyShowRobotCommentsWithHumanReply?: boolean,
- selectedAuthors?: AccountInfo[]
- ) {
- const threads = displayedThreads.filter(t =>
- this._shouldShowThread(
- t,
- unresolvedOnly,
- draftsOnly,
- onlyShowRobotCommentsWithHumanReply,
- selectedAuthors
- )
- );
- const index = threads.findIndex(t => t.rootId === thread.rootId);
- if (index === -1) {
- return false;
- }
- return index === 0 || threads[index - 1].path !== threads[index].path;
+ /**
+ * Returns all threads that are currently shown in the list, respecting the
+ * currently selected filter.
+ */
+ // private, but visible for testing
+ getDisplayedThreads() {
+ const byTimestamp =
+ this.sortDropdownValue === SortDropdownState.TIMESTAMP &&
+ !this.hideDropdown;
+ return this.getAllThreads()
+ .sort((t1, t2) => compareThreads(t1, t2, byTimestamp))
+ .filter(t => this.shouldShowThread(t));
}
- _shouldRenderSeparator(
- displayedThreads: CommentThread[],
- thread: CommentThread,
- unresolvedOnly?: boolean,
- draftsOnly?: boolean,
- onlyShowRobotCommentsWithHumanReply?: boolean,
- selectedAuthors?: AccountInfo[]
- ) {
- const threads = displayedThreads.filter(t =>
- this._shouldShowThread(
- t,
- unresolvedOnly,
- draftsOnly,
- onlyShowRobotCommentsWithHumanReply,
- selectedAuthors
- )
- );
- const index = threads.findIndex(t => t.rootId === thread.rootId);
- if (index === -1) {
- return false;
- }
- return (
- index > 0 &&
- this._isFirstThreadWithFileName(
- displayedThreads,
- thread,
- unresolvedOnly,
- draftsOnly,
- onlyShowRobotCommentsWithHumanReply,
- selectedAuthors
- )
+ private isASelectedAuthor(account?: AccountInfo) {
+ if (!account) return false;
+ return this.selectedAuthors.some(
+ author => account._account_id === author._account_id
);
}
- _shouldShowThread(
- thread: CommentThread,
- unresolvedOnly?: boolean,
- draftsOnly?: boolean,
- onlyShowRobotCommentsWithHumanReply?: boolean,
- selectedAuthors?: AccountInfo[]
- ) {
- if (
- [
- thread,
- unresolvedOnly,
- draftsOnly,
- onlyShowRobotCommentsWithHumanReply,
- selectedAuthors,
- ].includes(undefined)
- ) {
- return false;
+ private shouldShowThread(thread: CommentThread) {
+ // Never make a thread disappear while the user is editing it.
+ assertIsDefined(thread.rootId, 'thread.rootId');
+ const el = this.queryThreadElement(thread.rootId);
+ if (el?.editing) return true;
+
+ if (this.selectedAuthors.length > 0) {
+ const hasACommentFromASelectedAuthor = thread.comments.some(c =>
+ this.isASelectedAuthor(c.author)
+ );
+ if (!hasACommentFromASelectedAuthor) return false;
}
- if (selectedAuthors!.length) {
- if (
- !thread.comments.some(
- c =>
- c.author &&
- selectedAuthors!.some(
- author => c.author!._account_id === author._account_id
- )
- )
- ) {
- return false;
- }
+ // This is probably redundant, because getAllThreads() filters this out.
+ if (this.onlyShowRobotCommentsWithHumanReply) {
+ if (isRobotThread(thread) && !hasHumanReply(thread)) return false;
}
- if (
- !draftsOnly &&
- !unresolvedOnly &&
- !onlyShowRobotCommentsWithHumanReply
- ) {
- return true;
- }
+ if (this.draftsOnly && !isDraftThread(thread)) return false;
+ if (this.unresolvedOnly && !isUnresolved(thread)) return false;
- const threadInfo = this._getThreadWithStatusInfo(thread);
-
- if (threadInfo.isEditing) {
- return true;
- }
-
- if (
- threadInfo.hasRobotComment &&
- onlyShowRobotCommentsWithHumanReply &&
- !threadInfo.hasHumanReplyToRobotComment
- ) {
- return false;
- }
-
- let filtersCheck = true;
- if (draftsOnly && unresolvedOnly) {
- filtersCheck = threadInfo.hasDraft && threadInfo.unresolved;
- } else if (draftsOnly) {
- filtersCheck = threadInfo.hasDraft;
- } else if (unresolvedOnly) {
- filtersCheck = threadInfo.unresolved;
- }
-
- return filtersCheck;
+ return true;
}
- _getThreadWithStatusInfo(thread: CommentThread): CommentThreadWithInfo {
- const comments = thread.comments;
- const lastComment = comments.length
- ? comments[comments.length - 1]
- : undefined;
- const hasRobotComment = isRobotThread(thread);
- const hasHumanReplyToRobotComment =
- hasRobotComment && hasHumanReply(thread);
- let updated = undefined;
- if (lastComment) {
- if (isDraft(lastComment)) updated = lastComment.__date;
- if (lastComment.updated) updated = parseDate(lastComment.updated);
- }
-
- return {
- thread,
- hasRobotComment,
- hasHumanReplyToRobotComment,
- unresolved: !!lastComment && !!lastComment.unresolved,
- isEditing: isDraft(lastComment) && !!lastComment.__editing,
- hasDraft: !!lastComment && isDraft(lastComment),
- updated,
- };
- }
-
- _isOnParent(side?: CommentSide) {
- // TODO(TS): That looks like a bug? CommentSide.REVISION will also be
- // classified as parent??
- return !!side;
- }
-
- _handleOnlyUnresolved() {
+ private handleOnlyUnresolved() {
this.unresolvedOnly = true;
- this._draftsOnly = false;
+ this.draftsOnly = false;
}
- _handleOnlyDrafts() {
- this._draftsOnly = true;
+ private handleOnlyDrafts() {
+ this.draftsOnly = true;
this.unresolvedOnly = false;
}
- _handleAllComments() {
- this._draftsOnly = false;
+ private handleAllComments() {
+ this.draftsOnly = false;
this.unresolvedOnly = false;
}
- _showAllComments(draftsOnly?: boolean, unresolvedOnly?: boolean) {
- return !draftsOnly && !unresolvedOnly;
- }
-
- _countUnresolved(threads?: CommentThread[]) {
- return (
- this.filterRobotThreadsWithoutHumanReply(threads)?.filter(isUnresolved)
- .length ?? 0
- );
- }
-
- _countAllThreads(threads?: CommentThread[]) {
- return this.filterRobotThreadsWithoutHumanReply(threads)?.length ?? 0;
- }
-
- _countDrafts(threads?: CommentThread[]) {
- return (
- this.filterRobotThreadsWithoutHumanReply(threads)?.filter(isDraftThread)
- .length ?? 0
- );
- }
-
- filterRobotThreadsWithoutHumanReply(threads?: CommentThread[]) {
- return threads?.filter(t => !isRobotThread(t) || hasHumanReply(t));
- }
-
- _commentTabStateChange(
- newValue?: CommentTabState,
- oldValue?: CommentTabState
- ) {
- if (!newValue || newValue === oldValue) return;
- let focusTo: string | undefined;
- switch (newValue) {
- case CommentTabState.UNRESOLVED:
- this._handleOnlyUnresolved();
- // input is null because it's not rendered yet.
- focusTo = '#unresolvedRadio';
- break;
- case CommentTabState.DRAFTS:
- this._handleOnlyDrafts();
- focusTo = '#draftsRadio';
- break;
- case CommentTabState.SHOW_ALL:
- this._handleAllComments();
- focusTo = '#allRadio';
- break;
- default:
- assertNever(newValue, 'Unsupported preferred state');
- }
- const selector = focusTo;
- window.setTimeout(() => {
- const input = this.shadowRoot?.querySelector<HTMLInputElement>(selector);
- input?.focus();
- }, 0);
+ private queryThreadElement(rootId: string): GrCommentThread | undefined {
+ const els = [...(this.threadElements ?? [])] as GrCommentThread[];
+ return els.find(el => el.rootId === rootId);
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
deleted file mode 100644
index 3eb28c9..0000000
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- #threads {
- display: block;
- }
- gr-comment-thread {
- display: block;
- margin-bottom: var(--spacing-m);
- }
- .header {
- align-items: center;
- background-color: var(--background-color-primary);
- border-bottom: 1px solid var(--border-color);
- border-top: 1px solid var(--border-color);
- display: flex;
- justify-content: left;
- padding: var(--spacing-s) var(--spacing-l);
- }
- .draftsOnly:not(.unresolvedOnly) gr-comment-thread[has-draft],
- .unresolvedOnly:not(.draftsOnly) gr-comment-thread[unresolved],
- .draftsOnly.unresolvedOnly gr-comment-thread[has-draft][unresolved] {
- display: block;
- }
- .thread-separator {
- border-top: 1px solid var(--border-color);
- margin-top: var(--spacing-xl);
- }
- .show-resolved-comments {
- box-shadow: none;
- padding-left: var(--spacing-m);
- }
- .partypopper{
- margin-right: var(--spacing-s);
- }
- gr-dropdown-list {
- --trigger-style-text-color: var(--primary-text-color);
- --trigger-style-font-family: var(--font-family);
- }
- .filter-text, .sort-text, .author-text {
- margin-right: var(--spacing-s);
- color: var(--deemphasized-text-color);
- }
- .author-text {
- margin-left: var(--spacing-m);
- }
- gr-account-label {
- --account-max-length: 120px;
- display: inline-block;
- user-select: none;
- --label-border-radius: 8px;
- margin: 0 var(--spacing-xs);
- padding: var(--spacing-xs) var(--spacing-m);
- line-height: var(--line-height-normal);
- cursor: pointer;
- }
- gr-account-label:focus {
- outline: none;
- }
- gr-account-label:hover,
- gr-account-label:hover {
- box-shadow: var(--elevation-level-1);
- cursor: pointer;
- }
- </style>
- <template is="dom-if" if="[[!hideDropdown]]">
- <div class="header">
- <span class="sort-text">Sort By:</span>
- <gr-dropdown-list
- id="sortDropdown"
- value="[[sortDropdownValue]]"
- on-value-change="handleSortDropdownValueChange"
- items="[[getSortDropdownEntires()]]"
- >
- </gr-dropdown-list>
- <span class="separator"></span>
- <span class="filter-text">Filter By:</span>
- <gr-dropdown-list
- id="filterDropdown"
- value="[[commentsDropdownValue]]"
- on-value-change="handleCommentsDropdownValueChange"
- items="[[getCommentsDropdownEntires(threads, loggedIn)]]"
- >
- </gr-dropdown-list>
- <template is="dom-if" if="[[_displayedThreads.length]]">
- <span class="author-text">From:</span>
- <template is="dom-repeat" items="[[getCommentAuthors(_displayedThreads, account)]]">
- <gr-account-label
- account="[[item]]"
- on-click="handleAccountClicked"
- selectionChipStyle
- selected="[[isSelected(item, selectedAuthors)]]"
- > </gr-account-label>
- </template>
- </template>
- </div>
- </template>
- <div id="threads" part="threads">
- <template
- is="dom-if"
- if="[[_showEmptyThreadsMessage(threads, _displayedThreads, unresolvedOnly)]]"
- >
- <div>
- <span>
- <template is="dom-if" if="[[_showPartyPopper(threads)]]">
- <span class="partypopper">\🎉</span>
- </template>
- [[_computeEmptyThreadsMessage(threads, _displayedThreads,
- unresolvedOnly)]]
- <template is="dom-if" if="[[_showResolvedCommentsButton(threads, _displayedThreads, unresolvedOnly)]]">
- <gr-button
- class="show-resolved-comments"
- link
- on-click="_handleResolvedCommentsMessageClick">
- [[_computeResolvedCommentsMessage(threads, _displayedThreads,
- unresolvedOnly, onlyShowRobotCommentsWithHumanReply)]]
- </gr-button>
- </template>
- </span>
- </div>
- </template>
- <template
- is="dom-repeat"
- items="[[_displayedThreads]]"
- as="thread"
- initial-count="10"
- target-framerate="60"
- >
- <template
- is="dom-if"
- if="[[_shouldRenderSeparator(_displayedThreads, thread, unresolvedOnly, _draftsOnly, onlyShowRobotCommentsWithHumanReply, selectedAuthors)]]"
- >
- <div class="thread-separator"></div>
- </template>
- <gr-comment-thread
- show-file-path=""
- show-ported-comment="[[thread.ported]]"
- show-comment-context="[[showCommentContext]]"
- change-num="[[changeNum]]"
- comments="[[thread.comments]]"
- diff-side="[[thread.diffSide]]"
- show-file-name="[[_isFirstThreadWithFileName(_displayedThreads, thread, unresolvedOnly, _draftsOnly, onlyShowRobotCommentsWithHumanReply, selectedAuthors)]]"
- project-name="[[change.project]]"
- is-on-parent="[[_isOnParent(thread.commentSide)]]"
- line-num="[[thread.line]]"
- patch-num="[[thread.patchNum]]"
- path="[[thread.path]]"
- root-id="{{thread.rootId}}"
- should-scroll-into-view="[[computeShouldScrollIntoView(thread.comments, scrollCommentId)]]"
- ></gr-comment-thread>
- </template>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js
deleted file mode 100644
index aab5cee..0000000
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.js
+++ /dev/null
@@ -1,673 +0,0 @@
-/**
- * @license
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-thread-list.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {SpecialFilePath} from '../../../constants/constants.js';
-import {CommentTabState} from '../../../types/events.js';
-import {__testOnly_SortDropdownState} from './gr-thread-list.js';
-import {queryAll} from '../../../test/test-utils.js';
-import {accountOrGroupKey} from '../../../utils/account-util.js';
-import {tap} from '@polymer/iron-test-helpers/mock-interactions';
-import {createAccountDetailWithId} from '../../../test/test-data-generators.js';
-
-const basicFixture = fixtureFromElement('gr-thread-list');
-
-suite('gr-thread-list tests', () => {
- let element;
-
- function getVisibleThreads() {
- return [...dom(element.root)
- .querySelectorAll('gr-comment-thread')]
- .filter(e => e.style.display !== 'none');
- }
-
- setup(async () => {
- element = basicFixture.instantiate();
- element.changeNum = 123;
- element.change = {
- project: 'testRepo',
- };
- element.threads = [
- {
- comments: [
- {
- path: '/COMMIT_MSG',
- author: {
- _account_id: 1000001,
- name: 'user',
- username: 'user',
- },
- patch_set: 4,
- id: 'ecf0b9fa_fe1a5f62',
- line: 5,
- updated: '1',
- message: 'test',
- unresolved: true,
- },
- {
- id: '503008e2_0ab203ee',
- path: '/COMMIT_MSG',
- line: 5,
- in_reply_to: 'ecf0b9fa_fe1a5f62',
- updated: '1',
- message: 'draft',
- unresolved: true,
- __draft: true,
- __draftID: '0.m683trwff68',
- __editing: false,
- patch_set: '2',
- },
- ],
- patchNum: 4,
- path: '/COMMIT_MSG',
- line: 5,
- rootId: 'ecf0b9fa_fe1a5f62',
- updated: '1',
- },
- {
- comments: [
- {
- path: 'test.txt',
- author: {
- _account_id: 1000002,
- name: 'user',
- username: 'user',
- },
- patch_set: 3,
- id: '09a9fb0a_1484e6cf',
- side: 'PARENT',
- updated: '2',
- message: 'Some comment on another patchset.',
- unresolved: false,
- },
- ],
- patchNum: 3,
- path: 'test.txt',
- rootId: '09a9fb0a_1484e6cf',
- updated: '2',
- commentSide: 'PARENT',
- },
- {
- comments: [
- {
- path: '/COMMIT_MSG',
- author: {
- _account_id: 1000002,
- name: 'user',
- username: 'user',
- },
- patch_set: 2,
- id: '8caddf38_44770ec1',
- updated: '3',
- message: 'Another unresolved comment',
- unresolved: false,
- },
- ],
- patchNum: 2,
- path: '/COMMIT_MSG',
- rootId: '8caddf38_44770ec1',
- updated: '3',
- },
- {
- comments: [
- {
- path: '/COMMIT_MSG',
- author: {
- _account_id: 1000003,
- name: 'user',
- username: 'user',
- },
- patch_set: 2,
- id: 'scaddf38_44770ec1',
- line: 4,
- updated: '4',
- message: 'Yet another unresolved comment',
- unresolved: true,
- },
- ],
- patchNum: 2,
- path: '/COMMIT_MSG',
- line: 4,
- rootId: 'scaddf38_44770ec1',
- updated: '4',
- },
- {
- comments: [
- {
- id: 'zcf0b9fa_fe1a5f62',
- path: '/COMMIT_MSG',
- line: 6,
- updated: '5',
- message: 'resolved draft',
- unresolved: false,
- __draft: true,
- __draftID: '0.m683trwff69',
- __editing: false,
- patch_set: '2',
- },
- ],
- patchNum: 4,
- path: '/COMMIT_MSG',
- line: 6,
- rootId: 'zcf0b9fa_fe1a5f62',
- updated: '5',
- },
- {
- comments: [
- {
- id: 'patchset_level_1',
- path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
- updated: '6',
- message: 'patchset comment 1',
- unresolved: false,
- __editing: false,
- patch_set: '2',
- },
- ],
- patchNum: 2,
- path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
- rootId: 'patchset_level_1',
- updated: '6',
- },
- {
- comments: [
- {
- id: 'patchset_level_2',
- path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
- updated: '7',
- message: 'patchset comment 2',
- unresolved: false,
- __editing: false,
- patch_set: '3',
- },
- ],
- patchNum: 3,
- path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
- rootId: 'patchset_level_2',
- updated: '7',
- },
- {
- comments: [
- {
- path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 4,
- id: 'rc1',
- line: 5,
- updated: '8',
- message: 'test',
- unresolved: true,
- robot_id: 'rc1',
- },
- ],
- patchNum: 4,
- path: '/COMMIT_MSG',
- line: 5,
- rootId: 'rc1',
- updated: '8',
- },
- {
- comments: [
- {
- path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 4,
- id: 'rc2',
- line: 7,
- updated: '9',
- message: 'test',
- unresolved: true,
- robot_id: 'rc2',
- },
- {
- path: '/COMMIT_MSG',
- author: {
- _account_id: 1000000,
- name: 'user',
- username: 'user',
- },
- patch_set: 4,
- id: 'c2_1',
- line: 5,
- updated: '10',
- message: 'test',
- unresolved: true,
- },
- ],
- patchNum: 4,
- path: '/COMMIT_MSG',
- line: 7,
- rootId: 'rc2',
- updated: '10',
- },
- ];
-
- // use flush to render all (bypass initial-count set on dom-repeat)
- await flush();
- });
-
- test('draft dropdown item only appears when logged in', () => {
- element.loggedIn = false;
- flush();
- assert.equal(element.getCommentsDropdownEntires(element.threads,
- element.loggedIn).length, 2);
- element.loggedIn = true;
- flush();
- assert.equal(element.getCommentsDropdownEntires(element.threads,
- element.loggedIn).length, 3);
- });
-
- test('show all threads by default', () => {
- assert.equal(dom(element.root)
- .querySelectorAll('gr-comment-thread').length, element.threads.length);
- assert.equal(getVisibleThreads().length, element.threads.length);
- });
-
- test('show unresolved threads if unresolvedOnly is set', async () => {
- element.unresolvedOnly = true;
- await flush();
- const unresolvedThreads = element.threads.filter(t => t.comments.some(
- c => c.unresolved
- ));
- assert.equal(getVisibleThreads().length, unresolvedThreads.length);
- });
-
- test('showing file name takes visible threads into account', () => {
- element.sortDropdownValue = __testOnly_SortDropdownState.FILES;
- assert.equal(element._isFirstThreadWithFileName(element._sortedThreads,
- element._sortedThreads[2], element.unresolvedOnly, element._draftsOnly,
- element.onlyShowRobotCommentsWithHumanReply, element.selectedAuthors),
- true);
- element.unresolvedOnly = true;
- assert.equal(element._isFirstThreadWithFileName(element._sortedThreads,
- element._sortedThreads[2], element.unresolvedOnly, element._draftsOnly,
- element.onlyShowRobotCommentsWithHumanReply, element.selectedAuthors),
- false);
- });
-
- test('onlyShowRobotCommentsWithHumanReply ', () => {
- element.onlyShowRobotCommentsWithHumanReply = true;
- flush();
- assert.equal(
- getVisibleThreads().length,
- element.threads.length - 1);
- assert.isNotOk(getVisibleThreads().find(th => th.rootId === 'rc1'));
- });
-
- suite('_compareThreads', () => {
- setup(() => {
- element.sortDropdownValue = __testOnly_SortDropdownState.FILES;
- });
-
- test('patchset comes before any other file', () => {
- const t1 = {thread: {path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS}};
- const t2 = {thread: {path: SpecialFilePath.COMMIT_MESSAGE}};
-
- t1.patchNum = t2.patchNum = 1;
- t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
- assert.equal(element._compareThreads(t1, t2), -1);
- assert.equal(element._compareThreads(t2, t1), 1);
-
- // assigning values to properties such that t2 should come first
- t1.patchNum = 1;
- t2.patchNum = 2;
- t1.unresolved = t1.hasDraft = false;
- t2.unresolved = t2.unresolved = true;
- assert.equal(element._compareThreads(t1, t2), -1);
- assert.equal(element._compareThreads(t2, t1), 1);
- });
-
- test('file path is compared lexicographically', () => {
- const t1 = {thread: {path: 'a.txt'}};
- const t2 = {thread: {path: 'b.txt'}};
- t1.patchNum = t2.patchNum = 1;
- t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
- assert.equal(element._compareThreads(t1, t2), -1);
- assert.equal(element._compareThreads(t2, t1), 1);
-
- t1.patchNum = 1;
- t2.patchNum = 2;
- t1.unresolved = t1.hasDraft = false;
- t2.unresolved = t2.unresolved = true;
- assert.equal(element._compareThreads(t1, t2), -1);
- assert.equal(element._compareThreads(t2, t1), 1);
- });
-
- test('patchset comments sorted by reverse patchset', () => {
- const t1 = {thread: {path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
- patchNum: 1}};
- const t2 = {thread: {path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
- patchNum: 2}};
- t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
- assert.equal(element._compareThreads(t1, t2), 1);
- assert.equal(element._compareThreads(t2, t1), -1);
-
- t1.unresolved = t1.hasDraft = false;
- t2.unresolved = t2.unresolved = true;
- assert.equal(element._compareThreads(t1, t2), 1);
- assert.equal(element._compareThreads(t2, t1), -1);
- });
-
- test('patchset comments with same patchset picks unresolved first', () => {
- const t1 = {thread: {path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
- patchNum: 1}, unresolved: true};
- const t2 = {thread: {path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
- patchNum: 1}, unresolved: false};
- t1.hasDraft = t2.hasDraft = false;
- assert.equal(element._compareThreads(t1, t2), -1);
- assert.equal(element._compareThreads(t2, t1), 1);
- });
-
- test('file level comment before line', () => {
- const t1 = {thread: {path: 'a.txt', line: 2}};
- const t2 = {thread: {path: 'a.txt'}};
- t1.patchNum = t2.patchNum = 1;
- t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
- assert.equal(element._compareThreads(t1, t2), 1);
- assert.equal(element._compareThreads(t2, t1), -1);
-
- // give preference to t1 in unresolved/draft properties
- t1.unresolved = t1.hasDraft = true;
- t2.unresolved = t2.unresolved = false;
- assert.equal(element._compareThreads(t1, t2), 1);
- assert.equal(element._compareThreads(t2, t1), -1);
- });
-
- test('comments sorted by line', () => {
- const t1 = {thread: {path: 'a.txt', line: 2}};
- const t2 = {thread: {path: 'a.txt', line: 3}};
- t1.patchNum = t2.patchNum = 1;
- t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
- assert.equal(element._compareThreads(t1, t2), -1);
- assert.equal(element._compareThreads(t2, t1), 1);
-
- t1.unresolved = t1.hasDraft = false;
- t2.unresolved = t2.unresolved = true;
- assert.equal(element._compareThreads(t1, t2), -1);
- assert.equal(element._compareThreads(t2, t1), 1);
- });
-
- test('comments on same line sorted by reverse patchset', () => {
- const t1 = {thread: {path: 'a.txt', line: 2, patchNum: 1}};
- const t2 = {thread: {path: 'a.txt', line: 2, patchNum: 2}};
- t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
- assert.equal(element._compareThreads(t1, t2), 1);
- assert.equal(element._compareThreads(t2, t1), -1);
-
- // give preference to t1 in unresolved/draft properties
- t1.unresolved = t1.hasDraft = true;
- t2.unresolved = t2.unresolved = false;
- assert.equal(element._compareThreads(t1, t2), 1);
- assert.equal(element._compareThreads(t2, t1), -1);
- });
-
- test('comments on same line & patchset sorted by unresolved first',
- () => {
- const t1 = {thread: {path: 'a.txt', line: 2, patchNum: 1},
- unresolved: true};
- const t2 = {thread: {path: 'a.txt', line: 2, patchNum: 1},
- unresolved: false};
- t1.patchNum = t2.patchNum = 1;
- assert.equal(element._compareThreads(t1, t2), -1);
- assert.equal(element._compareThreads(t2, t1), 1);
-
- t2.hasDraft = true;
- t1.hasDraft = false;
- assert.equal(element._compareThreads(t1, t2), -1);
- assert.equal(element._compareThreads(t2, t1), 1);
- });
-
- test('comments on same line & patchset & unresolved sorted by draft',
- () => {
- const t1 = {thread: {path: 'a.txt', line: 2, patchNum: 1},
- unresolved: true, hasDraft: false};
- const t2 = {thread: {path: 'a.txt', line: 2, patchNum: 1},
- unresolved: true, hasDraft: true};
- t1.patchNum = t2.patchNum = 1;
- assert.equal(element._compareThreads(t1, t2), 1);
- assert.equal(element._compareThreads(t2, t1), -1);
- });
- });
-
- test('_computeSortedThreads', () => {
- element.sortDropdownValue = __testOnly_SortDropdownState.FILES;
- assert.equal(element._sortedThreads.length, 9);
- const expectedSortedRootIds = [
- 'patchset_level_2', // Posted on Patchset 3
- 'patchset_level_1', // Posted on Patchset 2
- '8caddf38_44770ec1', // File level on COMMIT_MSG
- 'scaddf38_44770ec1', // Line 4 on COMMIT_MSG
- 'ecf0b9fa_fe1a5f62', // Line 5 on COMMIT_MESSAGE but with drafts
- 'rc1', // Line 5 on COMMIT_MESSAGE without drafts
- 'zcf0b9fa_fe1a5f62', // Line 6 on COMMIT_MSG
- 'rc2', // Line 7 on COMMIT_MSG
- '09a9fb0a_1484e6cf', // File level on test.txt
- ];
- element._sortedThreads.forEach((thread, index) => {
- assert.equal(thread.rootId, expectedSortedRootIds[index]);
- });
- });
-
- test('_computeSortedThreads with timestamp', () => {
- element.sortDropdownValue = __testOnly_SortDropdownState.TIMESTAMP;
- element.resortThreads(element.threads);
- assert.equal(element._sortedThreads.length, 9);
- const expectedSortedRootIds = [
- 'rc2',
- 'rc1',
- 'patchset_level_2',
- 'patchset_level_1',
- 'zcf0b9fa_fe1a5f62',
- 'scaddf38_44770ec1',
- '8caddf38_44770ec1',
- '09a9fb0a_1484e6cf',
- 'ecf0b9fa_fe1a5f62',
- ];
- element._sortedThreads.forEach((thread, index) => {
- assert.equal(thread.rootId, expectedSortedRootIds[index]);
- });
- });
-
- test('tapping single author chips', () => {
- element.account = createAccountDetailWithId(1);
- flush();
- const chips = Array.from(queryAll(element, 'gr-account-label'));
- const authors = chips.map(
- chip => accountOrGroupKey(chip.account))
- .sort();
- assert.deepEqual(authors, [1, 1000000, 1000001, 1000002, 1000003]);
- assert.equal(element.threads.length, 9);
- assert.equal(element._displayedThreads.length, 9);
-
- // accountId 1000001
- const chip = chips.find(chip => chip.account._account_id === 1000001);
-
- tap(chip);
- flush();
-
- assert.equal(element.threads.length, 9);
- assert.equal(element._displayedThreads.length, 1);
- assert.equal(element._displayedThreads[0].comments[0].author._account_id,
- 1000001);
-
- tap(chip); // tapping again resets
- flush();
- assert.equal(element.threads.length, 9);
- assert.equal(element._displayedThreads.length, 9);
- });
-
- test('tapping multiple author chips', () => {
- element.account = createAccountDetailWithId(1);
- flush();
- const chips = Array.from(queryAll(element, 'gr-account-label'));
-
- tap(chips.find(chip => chip.account._account_id === 1000001));
- tap(chips.find(chip => chip.account._account_id === 1000002));
- flush();
-
- assert.equal(element.threads.length, 9);
- assert.equal(element._displayedThreads.length, 3);
- assert.equal(element._displayedThreads[0].comments[0].author._account_id,
- 1000002);
- assert.equal(element._displayedThreads[1].comments[0].author._account_id,
- 1000002);
- assert.equal(element._displayedThreads[2].comments[0].author._account_id,
- 1000001);
- });
-
- test('thread removal and sort again', () => {
- element.sortDropdownValue = __testOnly_SortDropdownState.FILES;
- const index = element.threads.findIndex(t => t.rootId === 'rc2');
- element.threads.splice(index, 1);
- element.threads = [...element.threads]; // trigger observers
- flush();
- assert.equal(element._sortedThreads.length, 8);
- const expectedSortedRootIds = [
- 'patchset_level_2',
- 'patchset_level_1',
- '8caddf38_44770ec1', // File level on COMMIT_MSG
- 'scaddf38_44770ec1', // Line 4 on COMMIT_MSG
- 'ecf0b9fa_fe1a5f62', // Line 5 on COMMIT_MESSAGE but with drafts
- 'rc1', // Line 5 on COMMIT_MESSAGE without drafts
- 'zcf0b9fa_fe1a5f62', // Line 6 on COMMIT_MSG
- '09a9fb0a_1484e6cf', // File level on test.txt
- ];
- element._sortedThreads.forEach((thread, index) => {
- assert.equal(thread.rootId, expectedSortedRootIds[index]);
- });
- });
-
- test('modification on thread shold not trigger sort again', () => {
- element.sortDropdownValue = __testOnly_SortDropdownState.FILES;
- const currentSortedThreads = [...element._sortedThreads];
- for (const thread of currentSortedThreads) {
- thread.comments = [...thread.comments];
- }
- const modifiedThreads = [...element.threads];
- modifiedThreads[5] = {...modifiedThreads[5]};
- modifiedThreads[5].comments = [...modifiedThreads[5].comments, {
- ...modifiedThreads[5].comments[0],
- unresolved: false,
- }];
- element.threads = modifiedThreads;
- assert.notDeepEqual(currentSortedThreads, element._sortedThreads);
-
- // exact same order as in _computeSortedThreads
- const expectedSortedRootIds = [
- 'patchset_level_2',
- 'patchset_level_1',
- '8caddf38_44770ec1', // File level on COMMIT_MSG
- 'scaddf38_44770ec1', // Line 4 on COMMIT_MSG
- 'ecf0b9fa_fe1a5f62', // Line 5 on COMMIT_MESSAGE but with drafts
- 'rc1', // Line 5 on COMMIT_MESSAGE without drafts
- 'zcf0b9fa_fe1a5f62', // Line 6 on COMMIT_MSG
- 'rc2', // Line 7 on COMMIT_MSG
- '09a9fb0a_1484e6cf', // File level on test.txt
- ];
- element._sortedThreads.forEach((thread, index) => {
- assert.equal(thread.rootId, expectedSortedRootIds[index]);
- });
- });
-
- test('reset sortedThreads when threads set to undefiend', () => {
- element.threads = undefined;
- assert.deepEqual(element._sortedThreads, []);
- });
-
- test('non-equal length of sortThreads and threads' +
- ' should trigger sort again', () => {
- element.sortDropdownValue = __testOnly_SortDropdownState.FILES;
- const modifiedThreads = [...element.threads];
- const currentSortedThreads = [...element._sortedThreads];
- element._sortedThreads = [];
- element.threads = modifiedThreads;
- assert.deepEqual(currentSortedThreads, element._sortedThreads);
-
- // exact same order as in _computeSortedThreads
- const expectedSortedRootIds = [
- 'patchset_level_2',
- 'patchset_level_1',
- '8caddf38_44770ec1', // File level on COMMIT_MSG
- 'scaddf38_44770ec1', // Line 4 on COMMIT_MSG
- 'ecf0b9fa_fe1a5f62', // Line 5 on COMMIT_MESSAGE but with drafts
- 'rc1', // Line 5 on COMMIT_MESSAGE without drafts
- 'zcf0b9fa_fe1a5f62', // Line 6 on COMMIT_MSG
- 'rc2', // Line 7 on COMMIT_MSG
- '09a9fb0a_1484e6cf', // File level on test.txt
- ];
- element._sortedThreads.forEach((thread, index) => {
- assert.equal(thread.rootId, expectedSortedRootIds[index]);
- });
- });
-
- test('show all comments', () => {
- element.handleCommentsDropdownValueChange({detail: {
- value: CommentTabState.SHOW_ALL}});
- flush();
- assert.equal(getVisibleThreads().length, 9);
- });
-
- test('unresolved shows all unresolved comments', () => {
- element.handleCommentsDropdownValueChange({detail: {
- value: CommentTabState.UNRESOLVED}});
- flush();
- assert.equal(getVisibleThreads().length, 4);
- });
-
- test('toggle drafts only shows threads with draft comments', () => {
- element.handleCommentsDropdownValueChange({detail: {
- value: CommentTabState.DRAFTS}});
- flush();
- assert.equal(getVisibleThreads().length, 2);
- });
-
- suite('hideDropdown', () => {
- setup(async () => {
- element.hideDropdown = true;
- await flush();
- });
-
- test('toggle buttons are hidden', () => {
- assert.equal(element.shadowRoot.querySelector('.header').style.display,
- 'none');
- });
- });
-
- suite('empty thread', () => {
- setup(async () => {
- element.threads = [];
- await flush();
- });
-
- test('default empty message should show', () => {
- assert.isTrue(
- element.shadowRoot.querySelector('#threads').textContent.trim()
- .includes('No comments'));
- });
- });
-});
-
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.ts
new file mode 100644
index 0000000..f6b9a81
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.ts
@@ -0,0 +1,516 @@
+/**
+ * @license
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-thread-list';
+import {CommentSide, SpecialFilePath} from '../../../constants/constants';
+import {CommentTabState} from '../../../types/events';
+import {
+ compareThreads,
+ GrThreadList,
+ __testOnly_SortDropdownState,
+} from './gr-thread-list';
+import {queryAll} from '../../../test/test-utils';
+import {accountOrGroupKey} from '../../../utils/account-util';
+import {tap} from '@polymer/iron-test-helpers/mock-interactions';
+import {
+ createAccountDetailWithId,
+ createParsedChange,
+ createThread,
+} from '../../../test/test-data-generators';
+import {
+ AccountId,
+ NumericChangeId,
+ PatchSetNum,
+ Timestamp,
+} from '../../../api/rest-api';
+import {RobotId, UrlEncodedCommentId} from '../../../types/common';
+import {CommentThread} from '../../../utils/comment-util';
+import {query, queryAndAssert} from '../../../utils/common-util';
+import {GrAccountLabel} from '../../shared/gr-account-label/gr-account-label';
+
+const basicFixture = fixtureFromElement('gr-thread-list');
+
+suite('gr-thread-list tests', () => {
+ let element: GrThreadList;
+
+ setup(async () => {
+ element = basicFixture.instantiate();
+ element.changeNum = 123 as NumericChangeId;
+ element.change = createParsedChange();
+ element.account = createAccountDetailWithId();
+ element.threads = [
+ {
+ comments: [
+ {
+ path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000001 as AccountId,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 4 as PatchSetNum,
+ id: 'ecf0b9fa_fe1a5f62' as UrlEncodedCommentId,
+ line: 5,
+ updated: '2015-12-01 15:15:15.000000000' as Timestamp,
+ message: 'test',
+ unresolved: true,
+ },
+ {
+ id: '503008e2_0ab203ee' as UrlEncodedCommentId,
+ path: '/COMMIT_MSG',
+ line: 5,
+ in_reply_to: 'ecf0b9fa_fe1a5f62' as UrlEncodedCommentId,
+ updated: '2015-12-01 15:16:15.000000000' as Timestamp,
+ message: 'draft',
+ unresolved: true,
+ __draft: true,
+ patch_set: '2' as PatchSetNum,
+ },
+ ],
+ patchNum: 4 as PatchSetNum,
+ path: '/COMMIT_MSG',
+ line: 5,
+ rootId: 'ecf0b9fa_fe1a5f62' as UrlEncodedCommentId,
+ commentSide: CommentSide.REVISION,
+ },
+ {
+ comments: [
+ {
+ path: 'test.txt',
+ author: {
+ _account_id: 1000002 as AccountId,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 3 as PatchSetNum,
+ id: '09a9fb0a_1484e6cf' as UrlEncodedCommentId,
+ updated: '2015-12-02 15:16:15.000000000' as Timestamp,
+ message: 'Some comment on another patchset.',
+ unresolved: false,
+ },
+ ],
+ patchNum: 3 as PatchSetNum,
+ path: 'test.txt',
+ rootId: '09a9fb0a_1484e6cf' as UrlEncodedCommentId,
+ commentSide: CommentSide.REVISION,
+ },
+ {
+ comments: [
+ {
+ path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000002 as AccountId,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 2 as PatchSetNum,
+ id: '8caddf38_44770ec1' as UrlEncodedCommentId,
+ updated: '2015-12-03 15:16:15.000000000' as Timestamp,
+ message: 'Another unresolved comment',
+ unresolved: false,
+ },
+ ],
+ patchNum: 2 as PatchSetNum,
+ path: '/COMMIT_MSG',
+ rootId: '8caddf38_44770ec1' as UrlEncodedCommentId,
+ commentSide: CommentSide.REVISION,
+ },
+ {
+ comments: [
+ {
+ path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000003 as AccountId,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 2 as PatchSetNum,
+ id: 'scaddf38_44770ec1' as UrlEncodedCommentId,
+ line: 4,
+ updated: '2015-12-04 15:16:15.000000000' as Timestamp,
+ message: 'Yet another unresolved comment',
+ unresolved: true,
+ },
+ ],
+ patchNum: 2 as PatchSetNum,
+ path: '/COMMIT_MSG',
+ line: 4,
+ rootId: 'scaddf38_44770ec1' as UrlEncodedCommentId,
+ commentSide: CommentSide.REVISION,
+ },
+ {
+ comments: [
+ {
+ id: 'zcf0b9fa_fe1a5f62' as UrlEncodedCommentId,
+ path: '/COMMIT_MSG',
+ line: 6,
+ updated: '2015-12-05 15:16:15.000000000' as Timestamp,
+ message: 'resolved draft',
+ unresolved: false,
+ __draft: true,
+ patch_set: '2' as PatchSetNum,
+ },
+ ],
+ patchNum: 4 as PatchSetNum,
+ path: '/COMMIT_MSG',
+ line: 6,
+ rootId: 'zcf0b9fa_fe1a5f62' as UrlEncodedCommentId,
+ commentSide: CommentSide.REVISION,
+ },
+ {
+ comments: [
+ {
+ id: 'patchset_level_1' as UrlEncodedCommentId,
+ path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
+ updated: '2015-12-06 15:16:15.000000000' as Timestamp,
+ message: 'patchset comment 1',
+ unresolved: false,
+ patch_set: '2' as PatchSetNum,
+ },
+ ],
+ patchNum: 2 as PatchSetNum,
+ path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
+ rootId: 'patchset_level_1' as UrlEncodedCommentId,
+ commentSide: CommentSide.REVISION,
+ },
+ {
+ comments: [
+ {
+ id: 'patchset_level_2' as UrlEncodedCommentId,
+ path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
+ updated: '2015-12-07 15:16:15.000000000' as Timestamp,
+ message: 'patchset comment 2',
+ unresolved: false,
+ patch_set: '3' as PatchSetNum,
+ },
+ ],
+ patchNum: 3 as PatchSetNum,
+ path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
+ rootId: 'patchset_level_2' as UrlEncodedCommentId,
+ commentSide: CommentSide.REVISION,
+ },
+ {
+ comments: [
+ {
+ path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000 as AccountId,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 4 as PatchSetNum,
+ id: 'rc1' as UrlEncodedCommentId,
+ line: 5,
+ updated: '2015-12-08 15:16:15.000000000' as Timestamp,
+ message: 'test',
+ unresolved: true,
+ robot_id: 'rc1' as RobotId,
+ },
+ ],
+ patchNum: 4 as PatchSetNum,
+ path: '/COMMIT_MSG',
+ line: 5,
+ rootId: 'rc1' as UrlEncodedCommentId,
+ commentSide: CommentSide.REVISION,
+ },
+ {
+ comments: [
+ {
+ path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000 as AccountId,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 4 as PatchSetNum,
+ id: 'rc2' as UrlEncodedCommentId,
+ line: 7,
+ updated: '2015-12-09 15:16:15.000000000' as Timestamp,
+ message: 'test',
+ unresolved: true,
+ robot_id: 'rc2' as RobotId,
+ },
+ {
+ path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000 as AccountId,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 4 as PatchSetNum,
+ id: 'c2_1' as UrlEncodedCommentId,
+ line: 5,
+ updated: '2015-12-10 15:16:15.000000000' as Timestamp,
+ message: 'test',
+ unresolved: true,
+ },
+ ],
+ patchNum: 4 as PatchSetNum,
+ path: '/COMMIT_MSG',
+ line: 7,
+ rootId: 'rc2' as UrlEncodedCommentId,
+ commentSide: CommentSide.REVISION,
+ },
+ ];
+ await element.updateComplete;
+ });
+
+ suite('sort threads', () => {
+ test('sort all threads', () => {
+ element.sortDropdownValue = __testOnly_SortDropdownState.FILES;
+ assert.equal(element.getDisplayedThreads().length, 9);
+ const expected: UrlEncodedCommentId[] = [
+ 'patchset_level_2' as UrlEncodedCommentId, // Posted on Patchset 3
+ 'patchset_level_1' as UrlEncodedCommentId, // Posted on Patchset 2
+ '8caddf38_44770ec1' as UrlEncodedCommentId, // File level on COMMIT_MSG
+ 'scaddf38_44770ec1' as UrlEncodedCommentId, // Line 4 on COMMIT_MSG
+ 'rc1' as UrlEncodedCommentId, // Line 5 on COMMIT_MESSAGE newer
+ 'ecf0b9fa_fe1a5f62' as UrlEncodedCommentId, // Line 5 on COMMIT_MESSAGE older
+ 'zcf0b9fa_fe1a5f62' as UrlEncodedCommentId, // Line 6 on COMMIT_MSG
+ 'rc2' as UrlEncodedCommentId, // Line 7 on COMMIT_MSG
+ '09a9fb0a_1484e6cf' as UrlEncodedCommentId, // File level on test.txt
+ ];
+ const actual = element.getDisplayedThreads().map(t => t.rootId);
+ assert.sameOrderedMembers(actual, expected);
+ });
+
+ test('sort all threads by timestamp', () => {
+ element.sortDropdownValue = __testOnly_SortDropdownState.TIMESTAMP;
+ assert.equal(element.getDisplayedThreads().length, 9);
+ const expected: UrlEncodedCommentId[] = [
+ 'rc2' as UrlEncodedCommentId,
+ 'rc1' as UrlEncodedCommentId,
+ 'patchset_level_2' as UrlEncodedCommentId,
+ 'patchset_level_1' as UrlEncodedCommentId,
+ 'zcf0b9fa_fe1a5f62' as UrlEncodedCommentId,
+ 'scaddf38_44770ec1' as UrlEncodedCommentId,
+ '8caddf38_44770ec1' as UrlEncodedCommentId,
+ '09a9fb0a_1484e6cf' as UrlEncodedCommentId,
+ 'ecf0b9fa_fe1a5f62' as UrlEncodedCommentId,
+ ];
+ const actual = element.getDisplayedThreads().map(t => t.rootId);
+ assert.sameOrderedMembers(actual, expected);
+ });
+ });
+
+ test('renders', async () => {
+ await element.updateComplete;
+ expect(element).shadowDom.to.equal(`
+ <div class="header">
+ <span class="sort-text">Sort By:</span>
+ <gr-dropdown-list id="sortDropdown"></gr-dropdown-list>
+ <span class="separator"></span>
+ <span class="filter-text">Filter By:</span>
+ <gr-dropdown-list id="filterDropdown"></gr-dropdown-list>
+ <span class="author-text">From:</span>
+ <gr-account-label deselected="" selectionchipstyle=""></gr-account-label>
+ <gr-account-label deselected="" selectionchipstyle=""></gr-account-label>
+ <gr-account-label deselected="" selectionchipstyle=""></gr-account-label>
+ <gr-account-label deselected="" selectionchipstyle=""></gr-account-label>
+ <gr-account-label deselected="" selectionchipstyle=""></gr-account-label>
+ </div>
+ <div id="threads" part="threads">
+ <gr-comment-thread show-file-name="" show-file-path=""></gr-comment-thread>
+ <gr-comment-thread show-file-path=""></gr-comment-thread>
+ <div class="thread-separator"></div>
+ <gr-comment-thread show-file-name="" show-file-path=""></gr-comment-thread>
+ <gr-comment-thread show-file-path=""></gr-comment-thread>
+ <div class="thread-separator"></div>
+ <gr-comment-thread has-draft="" show-file-name="" show-file-path=""></gr-comment-thread>
+ <gr-comment-thread show-file-path=""></gr-comment-thread>
+ <gr-comment-thread show-file-path=""></gr-comment-thread>
+ <div class="thread-separator"></div>
+ <gr-comment-thread show-file-name="" show-file-path=""></gr-comment-thread>
+ <div class="thread-separator"></div>
+ <gr-comment-thread has-draft="" show-file-name="" show-file-path=""></gr-comment-thread>
+ </div>
+ `);
+ });
+
+ test('renders empty', async () => {
+ element.threads = [];
+ await element.updateComplete;
+ expect(queryAndAssert(element, 'div#threads')).dom.to.equal(`
+ <div id="threads" part="threads">
+ <div><span>No comments</span></div>
+ </div>
+ `);
+ });
+
+ test('tapping single author chips', async () => {
+ element.account = createAccountDetailWithId(1);
+ await element.updateComplete;
+ const chips = Array.from(
+ queryAll<GrAccountLabel>(element, 'gr-account-label')
+ );
+ const authors = chips.map(chip => accountOrGroupKey(chip.account!)).sort();
+ assert.deepEqual(authors, [
+ 1 as AccountId,
+ 1000000 as AccountId,
+ 1000001 as AccountId,
+ 1000002 as AccountId,
+ 1000003 as AccountId,
+ ]);
+ assert.equal(element.threads.length, 9);
+ assert.equal(element.getDisplayedThreads().length, 9);
+
+ const chip = chips.find(chip => chip.account!._account_id === 1000001);
+ tap(chip!);
+ await element.updateComplete;
+
+ assert.equal(element.threads.length, 9);
+ assert.equal(element.getDisplayedThreads().length, 1);
+ assert.equal(
+ element.getDisplayedThreads()[0].comments[0].author?._account_id,
+ 1000001 as AccountId
+ );
+
+ tap(chip!);
+ await element.updateComplete;
+ assert.equal(element.threads.length, 9);
+ assert.equal(element.getDisplayedThreads().length, 9);
+ });
+
+ test('tapping multiple author chips', async () => {
+ element.account = createAccountDetailWithId(1);
+ await element.updateComplete;
+ const chips = Array.from(
+ queryAll<GrAccountLabel>(element, 'gr-account-label')
+ );
+
+ tap(chips.find(chip => chip.account?._account_id === 1000001)!);
+ tap(chips.find(chip => chip.account?._account_id === 1000002)!);
+ await element.updateComplete;
+
+ assert.equal(element.threads.length, 9);
+ assert.equal(element.getDisplayedThreads().length, 3);
+ assert.equal(
+ element.getDisplayedThreads()[0].comments[0].author?._account_id,
+ 1000002 as AccountId
+ );
+ assert.equal(
+ element.getDisplayedThreads()[1].comments[0].author?._account_id,
+ 1000002 as AccountId
+ );
+ assert.equal(
+ element.getDisplayedThreads()[2].comments[0].author?._account_id,
+ 1000001 as AccountId
+ );
+ });
+
+ test('show all comments', async () => {
+ const event = new CustomEvent('value-changed', {
+ detail: {value: CommentTabState.SHOW_ALL},
+ });
+ element.handleCommentsDropdownValueChange(event);
+ await element.updateComplete;
+ assert.equal(element.getDisplayedThreads().length, 9);
+ });
+
+ test('unresolved shows all unresolved comments', async () => {
+ const event = new CustomEvent('value-changed', {
+ detail: {value: CommentTabState.UNRESOLVED},
+ });
+ element.handleCommentsDropdownValueChange(event);
+ await element.updateComplete;
+ assert.equal(element.getDisplayedThreads().length, 4);
+ });
+
+ test('toggle drafts only shows threads with draft comments', async () => {
+ const event = new CustomEvent('value-changed', {
+ detail: {value: CommentTabState.DRAFTS},
+ });
+ element.handleCommentsDropdownValueChange(event);
+ await element.updateComplete;
+ assert.equal(element.getDisplayedThreads().length, 2);
+ });
+
+ suite('hideDropdown', () => {
+ test('header hidden for hideDropdown=true', async () => {
+ element.hideDropdown = true;
+ await element.updateComplete;
+ assert.isUndefined(query(element, '.header'));
+ });
+
+ test('header shown for hideDropdown=false', async () => {
+ element.hideDropdown = false;
+ await element.updateComplete;
+ assert.isDefined(query(element, '.header'));
+ });
+ });
+
+ suite('empty thread', () => {
+ setup(async () => {
+ element.threads = [];
+ await element.updateComplete;
+ });
+
+ test('default empty message should show', () => {
+ const threadsEl = queryAndAssert(element, '#threads');
+ assert.isTrue(threadsEl.textContent?.trim().includes('No comments'));
+ });
+ });
+});
+
+suite('compareThreads', () => {
+ let t1: CommentThread;
+ let t2: CommentThread;
+
+ const sortPredicate = (thread1: CommentThread, thread2: CommentThread) =>
+ compareThreads(thread1, thread2);
+
+ const checkOrder = (expected: CommentThread[]) => {
+ assert.sameOrderedMembers([t1, t2].sort(sortPredicate), expected);
+ assert.sameOrderedMembers([t2, t1].sort(sortPredicate), expected);
+ };
+
+ setup(() => {
+ t1 = createThread({});
+ t2 = createThread({});
+ });
+
+ test('patchset-level before file comments', () => {
+ t1.path = SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
+ t2.path = SpecialFilePath.COMMIT_MESSAGE;
+ checkOrder([t1, t2]);
+ });
+
+ test('paths lexicographically', () => {
+ t1.path = 'a.txt';
+ t2.path = 'b.txt';
+ checkOrder([t1, t2]);
+ });
+
+ test('patchsets in reverse order', () => {
+ t1.patchNum = 2 as PatchSetNum;
+ t2.patchNum = 3 as PatchSetNum;
+ checkOrder([t2, t1]);
+ });
+
+ test('file level comment before line', () => {
+ t1.line = 123;
+ t2.line = 'FILE';
+ checkOrder([t2, t1]);
+ });
+
+ test('comments sorted by line', () => {
+ t1.line = 123;
+ t2.line = 321;
+ checkOrder([t1, t2]);
+ });
+});
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-action.ts b/polygerrit-ui/app/elements/checks/gr-checks-action.ts
index 859fd33..bec6e08 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-action.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-action.ts
@@ -18,7 +18,7 @@
import {customElement, property} from 'lit/decorators';
import {Action} from '../../api/checks';
import {checkRequiredProperty} from '../../utils/common-util';
-import {appContext} from '../../services/app-context';
+import {getAppContext} from '../../services/app-context';
@customElement('gr-checks-action')
export class GrChecksAction extends LitElement {
@@ -28,7 +28,7 @@
@property({type: Object})
eventTarget: HTMLElement | null = null;
- private checksService = appContext.checksService;
+ private checksModel = getAppContext().checksModel;
override connectedCallback() {
super.connectedCallback();
@@ -72,7 +72,7 @@
private renderTooltip() {
if (!this.action.tooltip) return;
return html`
- <paper-tooltip offset="5" fit-to-visible-bounds>
+ <paper-tooltip offset="5" ?fitToVisibleBounds=${true}>
${this.action.tooltip}
</paper-tooltip>
`;
@@ -80,7 +80,7 @@
handleClick(e: Event) {
e.stopPropagation();
- this.checksService.triggerAction(this.action);
+ this.checksModel.triggerAction(this.action);
}
}
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 9c27cdb..7f66423 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -32,14 +32,7 @@
Tag,
} from '../../api/checks';
import {sharedStyles} from '../../styles/shared-styles';
-import {
- CheckRun,
- checksSelectedPatchsetNumber$,
- RunResult,
- someProvidersAreLoadingSelected$,
- topLevelActionsSelected$,
- topLevelLinksSelected$,
-} from '../../services/checks/checks-model';
+import {CheckRun, RunResult} from '../../services/checks/checks-model';
import {
allResults,
firstPrimaryLink,
@@ -62,9 +55,7 @@
LabelNameToInfoMap,
PatchSetNumber,
} from '../../types/common';
-import {labels$, latestPatchNum$} from '../../services/change/change-model';
-import {appContext} from '../../services/app-context';
-import {repoConfig$} from '../../services/config/config-model';
+import {getAppContext} from '../../services/app-context';
import {spinnerStyles} from '../../styles/gr-spinner-styles';
import {
getLabelStatus,
@@ -75,6 +66,23 @@
import {DropdownLink} from '../shared/gr-dropdown/gr-dropdown';
import {subscribe} from '../lit/subscription-controller';
import {fontStyles} from '../../styles/gr-font-styles';
+import {fire} from '../../utils/event-util';
+import {resolve} from '../../models/dependency';
+import {configModelToken} from '../../models/config/config-model';
+
+/**
+ * Firing this event sets the regular expression of the results filter.
+ */
+export interface ChecksResultsFilterDetail {
+ filterRegExp?: string;
+}
+export type ChecksResultsFilterEvent = CustomEvent<ChecksResultsFilterDetail>;
+
+declare global {
+ interface HTMLElementEventMap {
+ 'checks-results-filter': ChecksResultsFilterEvent;
+ }
+}
@customElement('gr-result-row')
class GrResultRow extends LitElement {
@@ -96,11 +104,13 @@
@state()
labels?: LabelNameToInfoMap;
- private checksService = appContext.checksService;
+ private changeModel = getAppContext().changeModel;
- constructor() {
- super();
- subscribe(this, labels$, x => (this.labels = x));
+ private checksModel = getAppContext().checksModel;
+
+ override connectedCallback() {
+ super.connectedCallback();
+ subscribe(this, this.changeModel.labels$, x => (this.labels = x));
}
static override get styles() {
@@ -215,6 +225,7 @@
background-color: var(--tag-background);
padding: 0 var(--spacing-m);
margin-left: var(--spacing-s);
+ cursor: pointer;
}
td .summary-cell .tag.gray {
background-color: var(--tag-gray);
@@ -386,6 +397,12 @@
this.toggleExpanded();
}
+ private tagClick(e: MouseEvent, tagName: string) {
+ e.preventDefault();
+ e.stopPropagation();
+ fire(this, 'checks-results-filter', {filterRegExp: tagName});
+ }
+
private toggleExpandedPress(e: KeyboardEvent) {
if (!this.isExpandable) return;
if (modifierPressed(e)) return;
@@ -494,7 +511,7 @@
}
private handleAction(e: CustomEvent<Action>) {
- this.checksService.triggerAction(e.detail);
+ this.checksModel.triggerAction(e.detail);
}
private renderAction(action?: Action) {
@@ -521,12 +538,16 @@
}
renderTag(tag: Tag) {
- return html`<div class="tag ${tag.color}">
+ return html`<button
+ class="tag ${tag.color}"
+ @click="${(e: MouseEvent) => this.tagClick(e, tag.name)}"
+ >
<span>${tag.name}</span>
<paper-tooltip offset="5" ?fitToVisibleBounds="${true}">
- ${tag.tooltip ?? 'A category tag for this check result'}
+ ${tag.tooltip ??
+ 'A category tag for this check result. Click to filter.'}
</paper-tooltip>
- </div>`;
+ </button>`;
}
}
@@ -538,7 +559,9 @@
@state()
repoConfig?: ConfigInfo;
- private changeService = appContext.changeService;
+ private changeModel = getAppContext().changeModel;
+
+ private configModel = resolve(this, configModelToken);
static override get styles() {
return [
@@ -561,9 +584,9 @@
];
}
- constructor() {
- super();
- subscribe(this, repoConfig$, x => (this.repoConfig = x));
+ override connectedCallback() {
+ super.connectedCallback();
+ subscribe(this, this.configModel().repoConfig$, x => (this.repoConfig = x));
}
override render() {
@@ -624,7 +647,7 @@
const end = pointer?.range?.end_line;
if (start) rangeText += `#${start}`;
if (end && start !== end) rangeText += `-${end}`;
- const change = this.changeService.getChange();
+ const change = this.changeModel.getChange();
assertIsDefined(change);
const path = pointer.path;
const patchset = this.result?.patchset as PatchSetNumber | undefined;
@@ -732,21 +755,35 @@
*/
private isSectionExpandedByUser = new Map<Category, boolean>();
- private readonly checksService = appContext.checksService;
+ private readonly changeModel = getAppContext().changeModel;
+
+ private readonly checksModel = getAppContext().checksModel;
constructor() {
super();
- subscribe(this, topLevelActionsSelected$, x => (this.actions = x));
- subscribe(this, topLevelLinksSelected$, x => (this.links = x));
subscribe(
this,
- checksSelectedPatchsetNumber$,
+ this.checksModel.topLevelActionsSelected$,
+ x => (this.actions = x)
+ );
+ subscribe(
+ this,
+ this.checksModel.topLevelLinksSelected$,
+ x => (this.links = x)
+ );
+ subscribe(
+ this,
+ this.checksModel.checksSelectedPatchsetNumber$,
x => (this.checksPatchsetNumber = x)
);
- subscribe(this, latestPatchNum$, x => (this.latestPatchsetNumber = x));
subscribe(
this,
- someProvidersAreLoadingSelected$,
+ this.changeModel.latestPatchNum$,
+ x => (this.latestPatchsetNumber = x)
+ );
+ subscribe(
+ this,
+ this.checksModel.someProvidersAreLoadingSelected$,
x => (this.someProvidersAreLoading = x)
);
}
@@ -883,6 +920,9 @@
.categoryHeader .statusIcon.success {
color: var(--success-foreground);
}
+ .categoryHeader.empty iron-icon.statusIcon {
+ color: var(--deemphasized-text-color);
+ }
.categoryHeader .filtered {
color: var(--deemphasized-text-color);
}
@@ -917,8 +957,13 @@
padding: var(--spacing-s);
}
tr.headerRow th.nameCol {
- width: 200px;
padding-left: var(--spacing-l);
+ width: 200px;
+ }
+ @media screen and (min-width: 1400px) {
+ tr.headerRow th.nameCol.longNames {
+ width: 300px;
+ }
}
tr.headerRow th.summaryCol {
width: 99%;
@@ -1095,7 +1140,15 @@
}
private handleAction(e: CustomEvent<Action>) {
- this.checksService.triggerAction(e.detail);
+ this.checksModel.triggerAction(e.detail);
+ }
+
+ private handleFilter(e: ChecksResultsFilterEvent) {
+ if (!this.filterInput) return;
+ const oldValue = this.filterInput.value ?? '';
+ const newValue = e.detail.filterRegExp ?? '';
+ this.filterInput.value = oldValue === newValue ? '' : newValue;
+ this.onFilterInputChange();
}
private renderAction(action?: Action) {
@@ -1106,11 +1159,11 @@
private onPatchsetSelected(e: CustomEvent<{value: string}>) {
const patchset = Number(e.detail.value);
check(!isNaN(patchset), 'selected patchset must be a number');
- this.checksService.setPatchset(patchset as PatchSetNumber);
+ this.checksModel.setPatchset(patchset as PatchSetNumber);
}
private goToLatestPatchset() {
- this.checksService.setPatchset(undefined);
+ this.checksModel.setPatchset(undefined);
}
private createPatchsetDropdownItems() {
@@ -1149,14 +1202,14 @@
<input
id="filterInput"
type="text"
- placeholder="Filter results by regular expression"
- @input="${this.onInput}"
+ placeholder="Filter results by tag or regular expression"
+ @input="${this.onFilterInputChange}"
/>
</div>
`;
}
- onInput() {
+ onFilterInputChange() {
assertIsDefined(this.filterInput, 'filter <input> element');
this.filterRegExp = new RegExp(this.filterInput.value, 'i');
}
@@ -1190,6 +1243,7 @@
const icon = expanded ? 'gr-icons:expand-less' : 'gr-icons:expand-more';
const isShowAll = this.isShowAll.get(category) ?? false;
const resultCount = filtered.length;
+ const empty = resultCount === 0 ? 'empty' : '';
const resultLimit = isShowAll ? 1000 : 20;
const showAllButton = this.renderShowAllButton(
category,
@@ -1200,7 +1254,7 @@
return html`
<div class="${expandedClass}">
<h3
- class="categoryHeader ${catString} heading-3"
+ class="categoryHeader ${catString} ${empty} heading-3"
@click="${() => this.toggleExpanded(category)}"
>
<iron-icon class="expandIcon" icon="${icon}"></iron-icon>
@@ -1274,16 +1328,20 @@
</div>`;
}
filtered = filtered.slice(0, limit);
+ // Some hosts/plugins use really long check names. If we have space and the
+ // check names are indeed very long, then set a more generous nameCol width.
+ const longestNameLength = Math.max(...all.map(r => r.checkName.length));
+ const nameColClasses = {nameCol: true, longNames: longestNameLength > 25};
return html`
<table class="resultsTable">
<thead>
<tr class="headerRow">
- <th class="nameCol">Run</th>
+ <th class="${classMap(nameColClasses)}">Run</th>
<th class="summaryCol">Summary</th>
<th class="expanderCol"></th>
</tr>
</thead>
- <tbody>
+ <tbody @checks-results-filter=${this.handleFilter}>
${repeat(
filtered,
result => result.internalResultId,
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index a643c18..4929b7c 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -33,11 +33,11 @@
worstCategory,
} from '../../services/checks/checks-util';
import {
- allRunsSelectedPatchset$,
CheckRun,
ChecksPatchset,
ErrorMessages,
- errorMessagesLatest$,
+} from '../../services/checks/checks-model';
+import {
fakeActions,
fakeLinks,
fakeRun0,
@@ -45,9 +45,8 @@
fakeRun2,
fakeRun3,
fakeRun4Att,
- loginCallbackLatest$,
- updateStateSetResults,
-} from '../../services/checks/checks-model';
+ fakeRun5,
+} from '../../services/checks/checks-fakes';
import {assertIsDefined} from '../../utils/common-util';
import {modifierPressed, whenVisible} from '../../utils/dom-util';
import {
@@ -57,7 +56,7 @@
} from './gr-checks-util';
import {ChecksTabState} from '../../types/events';
import {charsOnly} from '../../utils/string-util';
-import {appContext} from '../../services/app-context';
+import {getAppContext} from '../../services/app-context';
import {KnownExperimentId} from '../../services/flags/flags';
import {subscribe} from '../lit/subscription-controller';
import {fontStyles} from '../../styles/gr-font-styles';
@@ -110,7 +109,8 @@
.chip.check-circle-outline {
border-left: var(--thick-border) solid var(--success-foreground);
}
- .chip.timelapse {
+ .chip.timelapse,
+ .chip.scheduled {
border-left: var(--thick-border) solid var(--border-color);
}
.chip.placeholder {
@@ -389,15 +389,27 @@
private isSectionExpanded = new Map<RunStatus, boolean>();
- private flagService = appContext.flagsService;
+ private flagService = getAppContext().flagsService;
- private checksService = appContext.checksService;
+ private checksModel = getAppContext().checksModel;
constructor() {
super();
- subscribe(this, allRunsSelectedPatchset$, x => (this.runs = x));
- subscribe(this, errorMessagesLatest$, x => (this.errorMessages = x));
- subscribe(this, loginCallbackLatest$, x => (this.loginCallback = x));
+ subscribe(
+ this,
+ this.checksModel.allRunsSelectedPatchset$,
+ x => (this.runs = x)
+ );
+ subscribe(
+ this,
+ this.checksModel.errorMessagesLatest$,
+ x => (this.errorMessages = x)
+ );
+ subscribe(
+ this,
+ this.checksModel.loginCallbackLatest$,
+ x => (this.loginCallback = x)
+ );
}
static override get styles() {
@@ -619,7 +631,7 @@
link
?disabled=${runButtonDisabled}
@click="${() => {
- actions.forEach(action => this.checksService.triggerAction(action));
+ actions.forEach(action => this.checksModel.triggerAction(action));
}}"
>Run Selected</gr-button
>
@@ -659,25 +671,93 @@
}
none() {
- updateStateSetResults('f0', [], [], [], ChecksPatchset.LATEST);
- updateStateSetResults('f1', [], [], [], ChecksPatchset.LATEST);
- updateStateSetResults('f2', [], [], [], ChecksPatchset.LATEST);
- updateStateSetResults('f3', [], [], [], ChecksPatchset.LATEST);
- updateStateSetResults('f4', [], [], [], ChecksPatchset.LATEST);
+ this.checksModel.updateStateSetResults(
+ 'f0',
+ [],
+ [],
+ [],
+ ChecksPatchset.LATEST
+ );
+ this.checksModel.updateStateSetResults(
+ 'f1',
+ [],
+ [],
+ [],
+ ChecksPatchset.LATEST
+ );
+ this.checksModel.updateStateSetResults(
+ 'f2',
+ [],
+ [],
+ [],
+ ChecksPatchset.LATEST
+ );
+ this.checksModel.updateStateSetResults(
+ 'f3',
+ [],
+ [],
+ [],
+ ChecksPatchset.LATEST
+ );
+ this.checksModel.updateStateSetResults(
+ 'f4',
+ [],
+ [],
+ [],
+ ChecksPatchset.LATEST
+ );
+ this.checksModel.updateStateSetResults(
+ 'f5',
+ [],
+ [],
+ [],
+ ChecksPatchset.LATEST
+ );
}
all() {
- updateStateSetResults(
+ this.checksModel.updateStateSetResults(
'f0',
[fakeRun0],
fakeActions,
fakeLinks,
ChecksPatchset.LATEST
);
- updateStateSetResults('f1', [fakeRun1], [], [], ChecksPatchset.LATEST);
- updateStateSetResults('f2', [fakeRun2], [], [], ChecksPatchset.LATEST);
- updateStateSetResults('f3', [fakeRun3], [], [], ChecksPatchset.LATEST);
- updateStateSetResults('f4', fakeRun4Att, [], [], ChecksPatchset.LATEST);
+ this.checksModel.updateStateSetResults(
+ 'f1',
+ [fakeRun1],
+ [],
+ [],
+ ChecksPatchset.LATEST
+ );
+ this.checksModel.updateStateSetResults(
+ 'f2',
+ [fakeRun2],
+ [],
+ [],
+ ChecksPatchset.LATEST
+ );
+ this.checksModel.updateStateSetResults(
+ 'f3',
+ [fakeRun3],
+ [],
+ [],
+ ChecksPatchset.LATEST
+ );
+ this.checksModel.updateStateSetResults(
+ 'f4',
+ fakeRun4Att,
+ [],
+ [],
+ ChecksPatchset.LATEST
+ );
+ this.checksModel.updateStateSetResults(
+ 'f5',
+ [fakeRun5],
+ [],
+ [],
+ ChecksPatchset.LATEST
+ );
}
toggle(
@@ -687,7 +767,7 @@
links: Link[] = []
) {
const newRuns = this.runs.includes(runs[0]) ? [] : runs;
- updateStateSetResults(
+ this.checksModel.updateStateSetResults(
plugin,
newRuns,
actions,
@@ -699,13 +779,21 @@
renderSection(status: RunStatus) {
const runs = this.runs
.filter(r => r.isLatestAttempt)
- .filter(r => r.status === status)
+ .filter(
+ r =>
+ r.status === status ||
+ (status === RunStatus.RUNNING && r.status === RunStatus.SCHEDULED)
+ )
.filter(r => this.filterRegExp.test(r.checkName))
.sort(compareByWorstCategory);
if (runs.length === 0) return;
const expanded = this.isSectionExpanded.get(status) ?? true;
const expandedClass = expanded ? 'expanded' : 'collapsed';
const icon = expanded ? 'gr-icons:expand-less' : 'gr-icons:expand-more';
+ let header = headerForStatus(status);
+ if (runs.some(r => r.status === RunStatus.SCHEDULED)) {
+ header = `${header} / ${headerForStatus(RunStatus.SCHEDULED)}`;
+ }
return html`
<div class="${status.toLowerCase()} ${expandedClass}">
<div
@@ -713,7 +801,7 @@
@click="${() => this.toggleExpanded(status)}"
>
<iron-icon class="expandIcon" icon="${icon}"></iron-icon>
- <h3 class="heading-3">${headerForStatus(status)}</h3>
+ <h3 class="heading-3">${header}</h3>
</div>
<div class="sectionRuns">${runs.map(run => this.renderRun(run))}</div>
</div>
@@ -770,6 +858,9 @@
<gr-button link @click="${() => this.toggle('f4', fakeRun4Att)}}"
>4</gr-button
>
+ <gr-button link @click="${() => this.toggle('f5', [fakeRun5])}"
+ >5</gr-button
+ >
<gr-button link @click="${this.all}">all</gr-button>
</div>
`;
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
index ed6117a..a9c30c5 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-tab.ts
@@ -17,21 +17,14 @@
import {LitElement, css, html, PropertyValues} from 'lit';
import {customElement, property, state} from 'lit/decorators';
import {Action} from '../../api/checks';
-import {
- CheckResult,
- CheckRun,
- allResultsSelected$,
- checksSelectedPatchsetNumber$,
- allRunsSelectedPatchset$,
-} from '../../services/checks/checks-model';
+import {CheckResult, CheckRun} from '../../services/checks/checks-model';
import './gr-checks-runs';
import './gr-checks-results';
-import {changeNum$, latestPatchNum$} from '../../services/change/change-model';
import {NumericChangeId, PatchSetNumber} from '../../types/common';
import {ActionTriggeredEvent} from '../../services/checks/checks-util';
import {AttemptSelectedEvent, RunSelectedEvent} from './gr-checks-util';
import {ChecksTabState} from '../../types/events';
-import {appContext} from '../../services/app-context';
+import {getAppContext} from '../../services/app-context';
import {subscribe} from '../lit/subscription-controller';
/**
@@ -68,19 +61,33 @@
number | undefined
>();
- private readonly checksService = appContext.checksService;
+ private readonly changeModel = getAppContext().changeModel;
+
+ private readonly checksModel = getAppContext().checksModel;
constructor() {
super();
- subscribe(this, allRunsSelectedPatchset$, x => (this.runs = x));
- subscribe(this, allResultsSelected$, x => (this.results = x));
subscribe(
this,
- checksSelectedPatchsetNumber$,
+ this.checksModel.allRunsSelectedPatchset$,
+ x => (this.runs = x)
+ );
+ subscribe(
+ this,
+ this.checksModel.allResultsSelected$,
+ x => (this.results = x)
+ );
+ subscribe(
+ this,
+ this.checksModel.checksSelectedPatchsetNumber$,
x => (this.checksPatchsetNumber = x)
);
- subscribe(this, latestPatchNum$, x => (this.latestPatchsetNumber = x));
- subscribe(this, changeNum$, x => (this.changeNum = x));
+ subscribe(
+ this,
+ this.changeModel.latestPatchNum$,
+ x => (this.latestPatchsetNumber = x)
+ );
+ subscribe(this, this.changeModel.changeNum$, x => (this.changeNum = x));
this.addEventListener('action-triggered', (e: ActionTriggeredEvent) =>
this.handleActionTriggered(e.detail.action, e.detail.run)
@@ -140,7 +147,7 @@
}
handleActionTriggered(action: Action, run?: CheckRun) {
- this.checksService.triggerAction(action, run);
+ this.checksModel.triggerAction(action, run);
}
handleRunSelected(e: RunSelectedEvent) {
diff --git a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
index 95b7157..fbaeb8c 100644
--- a/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
+++ b/polygerrit-ui/app/elements/checks/gr-hovercard-run.ts
@@ -334,8 +334,15 @@
}
private computeChipIcon() {
- if (this.run?.status === RunStatus.COMPLETED) return 'check';
- if (this.run?.status === RunStatus.RUNNING) return 'timelapse';
+ if (this.run?.status === RunStatus.COMPLETED) {
+ return 'check';
+ }
+ if (this.run?.status === RunStatus.RUNNING) {
+ return iconFor(RunStatus.RUNNING);
+ }
+ if (this.run?.status === RunStatus.SCHEDULED) {
+ return iconFor(RunStatus.SCHEDULED);
+ }
return '';
}
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
index ac3d5f4..b55d5a4 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.ts
@@ -18,7 +18,7 @@
import '../../shared/gr-avatar/gr-avatar';
import {getUserName} from '../../../utils/display-name-util';
import {AccountInfo, ServerInfo} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {fireEvent} from '../../../utils/event-util';
import {
DropdownContent,
@@ -53,7 +53,7 @@
@property({type: String})
_switchAccountUrl = '';
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
override connectedCallback() {
super.connectedCallback();
@@ -83,6 +83,7 @@
gr-dropdown {
padding: 0 var(--spacing-m);
--gr-button-text-color: var(--header-text-color);
+ --gr-dropdown-item-color: var(--primary-text-color);
}
gr-avatar {
height: 2em;
@@ -94,36 +95,23 @@
}
override render() {
- // To pass CSS mixins for @apply to Polymer components, they need to appear
- // in <style> inside the template.
- /* eslint-disable lit/prefer-static-styles */
- const customStyle = html`
- <style>
- gr-dropdown {
- --gr-dropdown-item: {
- color: var(--primary-text-color);
- }
- }
- </style>
- `;
- return html`${customStyle}
- <gr-dropdown
- link=""
- .items="${this.links}"
- .topContent="${this.topContent}"
- @tap-item-shortcuts=${this._handleShortcutsTap}
- .horizontalAlign=${'right'}
+ return html`<gr-dropdown
+ link=""
+ .items="${this.links}"
+ .topContent="${this.topContent}"
+ @tap-item-shortcuts=${this._handleShortcutsTap}
+ .horizontalAlign=${'right'}
+ >
+ <span ?hidden="${this._hasAvatars}"
+ >${this._accountName(this.account)}</span
>
- <span ?hidden="${this._hasAvatars}"
- >${this._accountName(this.account)}</span
- >
- <gr-avatar
- .account="${this.account}"
- ?hidden=${!this._hasAvatars}
- .imageSize=${56}
- aria-label="Account avatar"
- ></gr-avatar>
- </gr-dropdown>`;
+ <gr-avatar
+ .account="${this.account}"
+ ?hidden=${!this._hasAvatars}
+ .imageSize=${56}
+ aria-label="Account avatar"
+ ></gr-avatar>
+ </gr-dropdown>`;
}
get links(): DropdownLink[] | undefined {
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 3b09d8c..41d9b24 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
@@ -22,7 +22,7 @@
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-error-manager_html';
import {getBaseUrl} from '../../../utils/url-util';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {IronA11yAnnouncer} from '@polymer/iron-a11y-announcer/iron-a11y-announcer';
import {customElement, property} from '@polymer/decorators';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
@@ -141,15 +141,15 @@
@property({type: String})
loginUrl = '/login';
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
- private readonly _authService = appContext.authService;
+ private readonly _authService = getAppContext().authService;
- private readonly eventEmitter = appContext.eventEmitter;
+ private readonly eventEmitter = getAppContext().eventEmitter;
_authErrorHandlerDeregistrationHook?: Function;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
private checkLoggedInTask?: DelayedTask;
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 80ebf2d..09c6a4e 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
@@ -23,7 +23,7 @@
__testOnly_ErrorType,
} from './gr-error-manager';
import {stubAuth, stubReporting, stubRestApi} from '../../../test/test-utils';
-import {appContext} from '../../../services/app-context';
+import {AppContext, getAppContext} from '../../../services/app-context';
import {
createAccountDetailWithId,
createPreferences,
@@ -41,11 +41,13 @@
let toastSpy: sinon.SinonSpy;
let fetchStub: sinon.SinonStub;
let getLoggedInStub: sinon.SinonStub;
+ let appContext: AppContext;
setup(() => {
fetchStub = stubAuth('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-key-binding-display/gr-key-binding-display.ts b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.ts
index 1ae0992..2a3936f 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.ts
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.ts
@@ -25,6 +25,9 @@
@customElement('gr-key-binding-display')
export class GrKeyBindingDisplay extends LitElement {
+ @property({type: Array})
+ binding: string[][] = [];
+
static override get styles() {
return [
css`
@@ -53,9 +56,6 @@
return html`${items}`;
}
- @property({type: Array})
- binding: string[][] = [];
-
_computeModifiers(binding: string[]) {
return binding.slice(0, binding.length - 1);
}
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts
index 8610999..70b1041 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.ts
@@ -16,16 +16,15 @@
*/
import '../../shared/gr-button/gr-button';
import '../gr-key-binding-display/gr-key-binding-display';
-import '../../../styles/shared-styles';
-import '../../../styles/gr-font-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-keyboard-shortcuts-dialog_html';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
import {
ShortcutSection,
SectionView,
} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
-import {property, customElement} from '@polymer/decorators';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {ShortcutViewListener} from '../../../services/shortcuts/shortcuts-service';
declare global {
@@ -40,11 +39,7 @@
}
@customElement('gr-keyboard-shortcuts-dialog')
-export class GrKeyboardShortcutsDialog extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrKeyboardShortcutsDialog extends LitElement {
/**
* Fired when the user presses the close button.
*
@@ -59,7 +54,7 @@
private readonly shortcutListener: ShortcutViewListener;
- private readonly shortcuts = appContext.shortcutsService;
+ private readonly shortcuts = getAppContext().shortcutsService;
constructor() {
super();
@@ -67,9 +62,107 @@
this._onDirectoryUpdated(d);
}
- override ready() {
- super.ready();
- this._ensureAttribute('role', 'dialog');
+ static override get styles() {
+ return [
+ sharedStyles,
+ fontStyles,
+ css`
+ :host {
+ display: block;
+ max-height: 100vh;
+ overflow-y: auto;
+ }
+ header {
+ padding: var(--spacing-l);
+ }
+ main {
+ display: flex;
+ padding: 0 var(--spacing-xxl) var(--spacing-xxl);
+ }
+ .column {
+ flex: 50%;
+ }
+ header {
+ align-items: center;
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ }
+ table caption {
+ font-weight: var(--font-weight-bold);
+ padding-top: var(--spacing-l);
+ text-align: left;
+ }
+ tr {
+ height: 32px;
+ }
+ td {
+ padding: var(--spacing-xs) 0;
+ }
+ td:first-child,
+ th:first-child {
+ padding-right: var(--spacing-m);
+ text-align: right;
+ width: 160px;
+ color: var(--deemphasized-text-color);
+ }
+ td:second-child {
+ min-width: 200px;
+ }
+ th {
+ color: var(--deemphasized-text-color);
+ text-align: left;
+ }
+ .header {
+ font-weight: var(--font-weight-bold);
+ padding-top: var(--spacing-l);
+ }
+ .modifier {
+ font-weight: var(--font-weight-normal);
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`<header>
+ <h3 class="heading-3">Keyboard shortcuts</h3>
+ <gr-button link="" @click=${this.handleCloseTap}>Close</gr-button>
+ </header>
+ <main>
+ <div class="column">
+ ${this._left?.map(section => this.renderSection(section))}
+ </div>
+ <div class="column">
+ ${this._right?.map(section => this.renderSection(section))}
+ </div>
+ </main>
+ <footer></footer>`;
+ }
+
+ private renderSection(section: SectionShortcut) {
+ return html`<table>
+ <caption>
+ ${section.section}
+ </caption>
+ <thead>
+ <tr>
+ <th>Key</th>
+ <th>Action</th>
+ </tr>
+ </thead>
+ <tbody>
+ ${section.shortcuts?.map(
+ shortcut => html`<tr>
+ <td>
+ <gr-key-binding-display .binding=${shortcut.binding}>
+ </gr-key-binding-display>
+ </td>
+ <td>${shortcut.text}</td>
+ </tr>`
+ )}
+ </tbody>
+ </table>`;
}
override connectedCallback() {
@@ -82,7 +175,7 @@
super.disconnectedCallback();
}
- _handleCloseTap(e: MouseEvent) {
+ private handleCloseTap(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
this.dispatchEvent(
@@ -142,7 +235,7 @@
});
}
- this.set('_left', left);
- this.set('_right', right);
+ this._right = right;
+ this._left = left;
}
}
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.ts b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.ts
deleted file mode 100644
index 4992daa..0000000
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-font-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- :host {
- display: block;
- max-height: 100vh;
- overflow-y: auto;
- }
- header {
- padding: var(--spacing-l);
- }
- main {
- display: flex;
- padding: 0 var(--spacing-xxl) var(--spacing-xxl);
- }
- .column {
- flex: 50%;
- }
- header {
- align-items: center;
- border-bottom: 1px solid var(--border-color);
- display: flex;
- justify-content: space-between;
- }
- table caption {
- font-weight: var(--font-weight-bold);
- padding-top: var(--spacing-l);
- text-align: left;
- }
- tr {
- height: 32px;
- }
- td {
- padding: var(--spacing-xs) 0;
- }
- td:first-child,
- th:first-child {
- padding-right: var(--spacing-m);
- text-align: right;
- width: 160px;
- color: var(--deemphasized-text-color);
- }
- td:second-child {
- min-width: 200px;
- }
- th {
- color: var(--deemphasized-text-color);
- text-align: left;
- }
- .header {
- font-weight: var(--font-weight-bold);
- padding-top: var(--spacing-l);
- }
- .modifier {
- font-weight: var(--font-weight-normal);
- }
- </style>
- <header>
- <h3 class="heading-3">Keyboard shortcuts</h3>
- <gr-button link="" on-click="_handleCloseTap">Close</gr-button>
- </header>
- <main>
- <div class="column">
- <template is="dom-repeat" items="[[_left]]">
- <table>
- <caption>
- [[item.section]]
- </caption>
- <thead>
- <tr>
- <th>Key</th>
- <th>Action</th>
- </tr>
- </thead>
- <tbody>
- <template is="dom-repeat" items="[[item.shortcuts]]" as="shortcut">
- <tr>
- <td>
- <gr-key-binding-display binding="[[shortcut.binding]]">
- </gr-key-binding-display>
- </td>
- <td>[[shortcut.text]]</td>
- </tr>
- </template>
- </tbody>
- </table>
- </template>
- </div>
- <div class="column">
- <template is="dom-repeat" items="[[_right]]">
- <table>
- <caption>
- [[item.section]]
- </caption>
- <thead>
- <tr>
- <th>Key</th>
- <th>Action</th>
- </tr>
- </thead>
- <tbody>
- <template is="dom-repeat" items="[[item.shortcuts]]" as="shortcut">
- <tr>
- <td>
- <gr-key-binding-display binding="[[shortcut.binding]]">
- </gr-key-binding-display>
- </td>
- <td>[[shortcut.text]]</td>
- </tr>
- </template>
- </tbody>
- </table>
- </template>
- </div>
- </main>
- <footer></footer>
-`;
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.ts b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.ts
index 2c76704..7fc52f5 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.ts
@@ -16,6 +16,7 @@
*/
import '../../../test/common-test-setup-karma';
+import './gr-keyboard-shortcuts-dialog';
import {GrKeyboardShortcutsDialog} from './gr-keyboard-shortcuts-dialog';
import {
SectionView,
@@ -27,8 +28,9 @@
suite('gr-keyboard-shortcuts-dialog tests', () => {
let element: GrKeyboardShortcutsDialog;
- setup(() => {
+ setup(async () => {
element = basicFixture.instantiate();
+ await flush();
});
function update(directory: Map<ShortcutSection, SectionView>) {
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 9d34929..5cc3737 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
@@ -14,17 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Subscription} from 'rxjs';
+import {map, distinctUntilChanged} from 'rxjs/operators';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../../shared/gr-dropdown/gr-dropdown';
import '../../shared/gr-icons/gr-icons';
import '../gr-account-dropdown/gr-account-dropdown';
import '../gr-smart-search/gr-smart-search';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-main-header_html';
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 {customElement, property} from '@polymer/decorators';
import {
AccountDetailInfo,
RequireProperties,
@@ -34,12 +33,13 @@
} from '../../../types/common';
import {AuthType} from '../../../constants/constants';
import {DropdownLink} from '../../shared/gr-dropdown/gr-dropdown';
-import {appContext} from '../../../services/app-context';
-import {Subject} from 'rxjs';
-import {serverConfig$} from '../../../services/config/config-model';
-import {takeUntil} from 'rxjs/operators';
-import {myTopMenuItems$} from '../../../services/user/user-model';
-import {assertIsDefined} from '../../../utils/common-util';
+import {getAppContext} from '../../../services/app-context';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, html, css} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+import {fireEvent} from '../../../utils/event-util';
+import {resolve} from '../../../models/dependency';
+import {configModelToken} from '../../../models/config/config-model';
type MainHeaderLink = RequireProperties<DropdownLink, 'url' | 'name'>;
@@ -103,107 +103,394 @@
AuthType.CUSTOM_EXTENSION,
]);
-@customElement('gr-main-header')
-export class GrMainHeader extends PolymerElement {
- static get template() {
- return htmlTemplate;
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-main-header': GrMainHeader;
}
+}
- @property({type: String, notify: true})
+@customElement('gr-main-header')
+export class GrMainHeader extends LitElement {
+ @property({type: String})
searchQuery = '';
- @property({type: Boolean, reflectToAttribute: true})
+ @property({type: Boolean, reflect: true})
loggedIn?: boolean;
- @property({type: Boolean, reflectToAttribute: true})
+ @property({type: Boolean, reflect: true})
loading?: boolean;
- @property({type: Object})
- _account?: AccountDetailInfo;
-
- @property({type: Array})
- _adminLinks: NavLink[] = [];
-
- @property({type: String})
- _docBaseUrl: string | null = null;
-
- @property({
- type: Array,
- computed: '_computeLinks(_userLinks, _adminLinks, _topMenus, _docBaseUrl)',
- })
- _links?: MainHeaderLinkGroup[];
-
@property({type: String})
loginUrl = '/login';
- @property({type: Array})
- _userLinks: MainHeaderLink[] = [];
-
- @property({type: Array})
- _topMenus?: TopMenuEntryInfo[] = [];
-
- @property({type: String})
- _registerText = 'Sign up';
-
- // Empty string means that the register <div> will be hidden.
- @property({type: String})
- _registerURL = '';
-
- @property({type: String})
- _feedbackURL = '';
-
@property({type: Boolean})
mobileSearchHidden = false;
- private readonly restApiService = appContext.restApiService;
+ // private but used in test
+ @state() account?: AccountDetailInfo;
- private readonly jsAPI = appContext.jsApiService;
+ @state() private adminLinks: NavLink[] = [];
- private readonly disconnected$ = new Subject();
+ @state() private docBaseUrl: string | null = null;
- override ready() {
- super.ready();
- this._ensureAttribute('role', 'banner');
- }
+ @state() private userLinks: MainHeaderLink[] = [];
+
+ @state() private topMenus?: TopMenuEntryInfo[] = [];
+
+ // private but used in test
+ @state() registerText = 'Sign up';
+
+ // Empty string means that the register <div> will be hidden.
+ // private but used in test
+ @state() registerURL = '';
+
+ // private but used in test
+ @state() feedbackURL = '';
+
+ @state() private serverConfig?: ServerInfo;
+
+ private readonly restApiService = getAppContext().restApiService;
+
+ private readonly jsAPI = getAppContext().jsApiService;
+
+ private readonly userModel = getAppContext().userModel;
+
+ private readonly configModel = resolve(this, configModelToken);
+
+ private subscriptions: Subscription[] = [];
override connectedCallback() {
- // TODO(brohlfs): This just ensures that the userService is instantiated at
- // all. We need the service to manage the model, but we are not making any
- // direct calls. Will need to find a better solution to this problem ...
- assertIsDefined(appContext.userService);
-
super.connectedCallback();
- this._loadAccount();
+ this.loadAccount();
- myTopMenuItems$.pipe(takeUntil(this.disconnected$)).subscribe(items => {
- this._userLinks = items.map(this._createHeaderLink);
- });
-
- serverConfig$.pipe(takeUntil(this.disconnected$)).subscribe(config => {
- if (!config) return;
- this._retrieveFeedbackURL(config);
- this._retrieveRegisterURL(config);
- getDocsBaseUrl(config, this.restApiService).then(docBaseUrl => {
- this._docBaseUrl = docBaseUrl;
- });
- });
+ this.subscriptions.push(
+ this.userModel.preferences$
+ .pipe(
+ map(preferences => preferences?.my ?? []),
+ distinctUntilChanged()
+ )
+ .subscribe(items => {
+ this.userLinks = items.map(this.createHeaderLink);
+ })
+ );
+ this.subscriptions.push(
+ this.configModel().serverConfig$.subscribe(config => {
+ if (!config) return;
+ this.serverConfig = config;
+ this.retrieveFeedbackURL(config);
+ this.retrieveRegisterURL(config);
+ getDocsBaseUrl(config, this.restApiService).then(docBaseUrl => {
+ this.docBaseUrl = docBaseUrl;
+ });
+ })
+ );
}
override disconnectedCallback() {
- this.disconnected$.next();
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions = [];
super.disconnectedCallback();
}
+ static override get styles() {
+ return [
+ sharedStyles,
+ css`
+ :host {
+ display: block;
+ }
+ nav {
+ align-items: center;
+ display: flex;
+ }
+ .bigTitle {
+ color: var(--header-text-color);
+ font-size: var(--header-title-font-size);
+ text-decoration: none;
+ }
+ .bigTitle:hover {
+ text-decoration: underline;
+ }
+ .titleText::before {
+ background-image: var(--header-icon);
+ background-size: var(--header-icon-size) var(--header-icon-size);
+ background-repeat: no-repeat;
+ content: '';
+ display: inline-block;
+ height: var(--header-icon-size);
+ margin-right: calc(var(--header-icon-size) / 4);
+ vertical-align: text-bottom;
+ width: var(--header-icon-size);
+ }
+ .titleText::after {
+ content: var(--header-title-content);
+ }
+ ul {
+ list-style: none;
+ padding-left: var(--spacing-l);
+ }
+ .links > li {
+ cursor: default;
+ display: inline-block;
+ padding: 0;
+ position: relative;
+ }
+ .linksTitle {
+ display: inline-block;
+ font-weight: var(--font-weight-bold);
+ position: relative;
+ text-transform: uppercase;
+ }
+ .linksTitle:hover {
+ opacity: 0.75;
+ }
+ .rightItems {
+ align-items: center;
+ display: flex;
+ flex: 1;
+ justify-content: flex-end;
+ }
+ .rightItems gr-endpoint-decorator:not(:empty) {
+ margin-left: var(--spacing-l);
+ }
+ gr-smart-search {
+ flex-grow: 1;
+ margin: 0 var(--spacing-m);
+ max-width: 500px;
+ min-width: 150px;
+ }
+ gr-dropdown,
+ .browse {
+ padding: var(--spacing-m);
+ }
+ gr-dropdown {
+ --gr-dropdown-item-color: var(--primary-text-color);
+ }
+ .settingsButton {
+ margin-left: var(--spacing-m);
+ }
+ .feedbackButton {
+ margin-left: var(--spacing-s);
+ }
+ .browse {
+ color: var(--header-text-color);
+ /* Same as gr-button */
+ margin: 5px 4px;
+ text-decoration: none;
+ }
+ .invisible,
+ .settingsButton,
+ gr-account-dropdown {
+ display: none;
+ }
+ :host([loading]) .accountContainer,
+ :host([loggedIn]) .loginButton,
+ :host([loggedIn]) .registerButton {
+ display: none;
+ }
+ :host([loggedIn]) .settingsButton,
+ :host([loggedIn]) gr-account-dropdown {
+ display: inline;
+ }
+ .accountContainer {
+ align-items: center;
+ display: flex;
+ margin: 0 calc(0 - var(--spacing-m)) 0 var(--spacing-m);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .loginButton,
+ .registerButton {
+ padding: var(--spacing-m) var(--spacing-l);
+ }
+ .dropdown-trigger {
+ text-decoration: none;
+ }
+ .dropdown-content {
+ background-color: var(--view-background-color);
+ box-shadow: var(--elevation-level-2);
+ }
+ /*
+ * We are not using :host to do this, because :host has a lowest css priority
+ * compared to others. This means that using :host to do this would break styles.
+ */
+ .linksTitle,
+ .bigTitle,
+ .loginButton,
+ .registerButton,
+ iron-icon,
+ gr-account-dropdown {
+ color: var(--header-text-color);
+ }
+ #mobileSearch {
+ display: none;
+ }
+ @media screen and (max-width: 50em) {
+ .bigTitle {
+ font-family: var(--header-font-family);
+ font-size: var(--font-size-h3);
+ font-weight: var(--font-weight-h3);
+ line-height: var(--line-height-h3);
+ }
+ gr-smart-search,
+ .browse,
+ .rightItems .hideOnMobile,
+ .links > li.hideOnMobile {
+ display: none;
+ }
+ #mobileSearch {
+ display: inline-flex;
+ }
+ .accountContainer {
+ margin-left: var(--spacing-m) !important;
+ }
+ gr-dropdown {
+ padding: var(--spacing-m) 0 var(--spacing-m) var(--spacing-m);
+ }
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`
+ <nav>
+ <a href="${`//${window.location.host}${getBaseUrl()}/`}" class="bigTitle">
+ <gr-endpoint-decorator name="header-title">
+ <span class="titleText"></span>
+ </gr-endpoint-decorator>
+ </a>
+ <ul class="links">
+ ${this.computeLinks(
+ this.userLinks,
+ this.adminLinks,
+ this.topMenus,
+ this.docBaseUrl
+ ).map(linkGroup => this.renderLinkGroup(linkGroup))}
+ </ul>
+ <div class="rightItems">
+ <gr-endpoint-decorator
+ class="hideOnMobile"
+ name="header-small-banner"
+ ></gr-endpoint-decorator>
+ <gr-smart-search
+ id="search"
+ label="Search for changes"
+ .searchQuery=${this.searchQuery}
+ .serverConfig=${this.serverConfig}
+ ></gr-smart-search>
+ <gr-endpoint-decorator
+ class="hideOnMobile"
+ name="header-browse-source"
+ ></gr-endpoint-decorator>
+ <gr-endpoint-decorator class="feedbackButton" name="header-feedback">
+ ${this.renderFeedback()}
+ </gr-endpoint-decorator>
+ </div>
+ ${this.renderAccount()}
+ </div>
+ </nav>
+ `;
+ }
+
+ private renderLinkGroup(linkGroup: MainHeaderLinkGroup) {
+ return html`
+ <li class="${linkGroup.class ?? ''}">
+ <gr-dropdown
+ link
+ down-arrow
+ .items=${linkGroup.links}
+ horizontal-align="left"
+ >
+ <span class="linksTitle" id="${linkGroup.title}">
+ ${linkGroup.title}
+ </span>
+ </gr-dropdown>
+ </li>
+ `;
+ }
+
+ private renderFeedback() {
+ if (!this.feedbackURL) return;
+
+ return html`
+ <a
+ href="${this.feedbackURL}"
+ title="File a bug"
+ aria-label="File a bug"
+ target="_blank"
+ role="button"
+ >
+ <iron-icon icon="gr-icons:bug"></iron-icon>
+ </a>
+ `;
+ }
+
+ private renderAccount() {
+ return html`
+ <div class="accountContainer" id="accountContainer">
+ <iron-icon
+ id="mobileSearch"
+ icon="gr-icons:search"
+ @click=${(e: Event) => {
+ this.onMobileSearchTap(e);
+ }}
+ role="button"
+ aria-label="${this.mobileSearchHidden
+ ? 'Show Searchbar'
+ : 'Hide Searchbar'}"
+ ></iron-icon>
+ ${this.renderRegister()}
+ <a class="loginButton" href="${this.loginUrl}">Sign in</a>
+ <a
+ class="settingsButton"
+ href="${getBaseUrl()}/settings/"
+ title="Settings"
+ aria-label="Settings"
+ role="button"
+ >
+ <iron-icon icon="gr-icons:settings"></iron-icon>
+ </a>
+ ${this.renderAccountDropdown()}
+ </div>
+ `;
+ }
+
+ private renderRegister() {
+ if (!this.registerURL) return;
+
+ return html`
+ <div class="registerDiv">
+ <a class="registerButton" href="${this.registerURL}">
+ ${this.registerText}
+ </a>
+ </div>
+ `;
+ }
+
+ private renderAccountDropdown() {
+ if (!this.account) return;
+
+ return html`
+ <gr-account-dropdown .account=${this.account}></gr-account-dropdown>
+ `;
+ }
+
+ override firstUpdated(changedProperties: PropertyValues) {
+ super.firstUpdated(changedProperties);
+ if (!this.getAttribute('role')) this.setAttribute('role', 'banner');
+ }
+
reload() {
- this._loadAccount();
+ this.loadAccount();
}
- _computeRelativeURL(path: string) {
- return '//' + window.location.host + getBaseUrl() + path;
- }
-
- _computeLinks(
- userLinks?: TopMenuItemInfo[],
+ // private but used in test
+ computeLinks(
+ userLinks?: MainHeaderLink[],
adminLinks?: NavLink[],
topMenus?: TopMenuEntryInfo[],
docBaseUrl?: string | null,
@@ -217,7 +504,7 @@
topMenus === undefined ||
docBaseUrl === undefined
) {
- return undefined;
+ return [];
}
const links: MainHeaderLinkGroup[] = defaultLinks.map(menu => {
@@ -232,7 +519,7 @@
links: userLinks.slice(),
});
}
- const docLinks = this._getDocLinks(docBaseUrl, DOCUMENTATION_LINKS);
+ const docLinks = this.getDocLinks(docBaseUrl, DOCUMENTATION_LINKS);
if (docLinks.length) {
links.push({
title: 'Documentation',
@@ -249,7 +536,7 @@
topMenuLinks[link.title] = link.links;
});
for (const m of topMenus) {
- const items = m.items.map(this._createHeaderLink).filter(
+ const items = m.items.map(this.createHeaderLink).filter(
link =>
// Ignore GWT project links
!link.url.includes('${projectName}')
@@ -268,7 +555,8 @@
return links;
}
- _getDocLinks(docBaseUrl: string | null, docLinks: MainHeaderLink[]) {
+ // private but used in test
+ getDocLinks(docBaseUrl: string | null, docLinks: MainHeaderLink[]) {
if (!docBaseUrl) {
return [];
}
@@ -285,7 +573,8 @@
});
}
- _loadAccount() {
+ // private but used in test
+ loadAccount() {
this.loading = true;
return Promise.all([
@@ -294,10 +583,10 @@
getPluginLoader().awaitPluginsLoaded(),
]).then(result => {
const account = result[0];
- this._account = account;
+ this.account = account;
this.loggedIn = !!account;
this.loading = false;
- this._topMenus = result[1];
+ this.topMenus = result[1];
return getAdminLinks(
account,
@@ -310,31 +599,30 @@
}),
() => this.jsAPI.getAdminMenuLinks()
).then(res => {
- this._adminLinks = res.links;
+ this.adminLinks = res.links;
});
});
}
- _retrieveFeedbackURL(config: ServerInfo) {
+ // private but used in test
+ retrieveFeedbackURL(config: ServerInfo) {
if (config.gerrit?.report_bug_url) {
- this._feedbackURL = config.gerrit.report_bug_url;
+ this.feedbackURL = config.gerrit.report_bug_url;
}
}
- _retrieveRegisterURL(config: ServerInfo) {
+ // private but used in test
+ retrieveRegisterURL(config: ServerInfo) {
if (AUTH_TYPES_WITH_REGISTER_URL.has(config.auth.auth_type)) {
- this._registerURL = config.auth.register_url ?? '';
+ this.registerURL = config.auth.register_url ?? '';
if (config.auth.register_text) {
- this._registerText = config.auth.register_text;
+ this.registerText = config.auth.register_text;
}
}
}
- _computeRegisterHidden(registerURL: string) {
- return !registerURL;
- }
-
- _createHeaderLink(linkObj: TopMenuItemInfo): MainHeaderLink {
+ // private but used in test
+ createHeaderLink(linkObj: TopMenuItemInfo): MainHeaderLink {
// Delete target property due to complications of
// https://bugs.chromium.org/p/gerrit/issues/detail?id=5888
//
@@ -353,36 +641,9 @@
return headerLink;
}
- _generateSettingsLink() {
- return getBaseUrl() + '/settings/';
- }
-
- _onMobileSearchTap(e: Event) {
+ private onMobileSearchTap(e: Event) {
e.preventDefault();
e.stopPropagation();
- this.dispatchEvent(
- new CustomEvent('mobile-search', {
- composed: true,
- bubbles: false,
- })
- );
- }
-
- _computeLinkGroupClass(linkGroup: MainHeaderLinkGroup) {
- return linkGroup.class ?? '';
- }
-
- _computeShowHideAriaLabel(mobileSearchHidden: boolean) {
- if (mobileSearchHidden) {
- return 'Show Searchbar';
- } else {
- return 'Hide Searchbar';
- }
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-main-header': GrMainHeader;
+ fireEvent(this, 'mobile-search');
}
}
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.ts
deleted file mode 100644
index b745c3d..0000000
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.ts
+++ /dev/null
@@ -1,259 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- display: block;
- }
- nav {
- align-items: center;
- display: flex;
- }
- .bigTitle {
- color: var(--header-text-color);
- font-size: var(--header-title-font-size);
- text-decoration: none;
- }
- .bigTitle:hover {
- text-decoration: underline;
- }
- .titleText::before {
- background-image: var(--header-icon);
- background-size: var(--header-icon-size) var(--header-icon-size);
- background-repeat: no-repeat;
- content: '';
- display: inline-block;
- height: var(--header-icon-size);
- margin-right: calc(var(--header-icon-size) / 4);
- vertical-align: text-bottom;
- width: var(--header-icon-size);
- }
- .titleText::after {
- content: var(--header-title-content);
- }
- ul {
- list-style: none;
- padding-left: var(--spacing-l);
- }
- .links > li {
- cursor: default;
- display: inline-block;
- padding: 0;
- position: relative;
- }
- .linksTitle {
- display: inline-block;
- font-weight: var(--font-weight-bold);
- position: relative;
- text-transform: uppercase;
- }
- .linksTitle:hover {
- opacity: 0.75;
- }
- .rightItems {
- align-items: center;
- display: flex;
- flex: 1;
- justify-content: flex-end;
- }
- .rightItems gr-endpoint-decorator:not(:empty) {
- margin-left: var(--spacing-l);
- }
- gr-smart-search {
- flex-grow: 1;
- margin: 0 var(--spacing-m);
- max-width: 500px;
- min-width: 150px;
- }
- gr-dropdown,
- .browse {
- padding: var(--spacing-m);
- }
- gr-dropdown {
- --gr-dropdown-item: {
- color: var(--primary-text-color);
- }
- }
- .settingsButton {
- margin-left: var(--spacing-m);
- }
- .feedbackButton {
- margin-left: var(--spacing-s);
- }
- .browse {
- color: var(--header-text-color);
- /* Same as gr-button */
- margin: 5px 4px;
- text-decoration: none;
- }
- .invisible,
- .settingsButton,
- gr-account-dropdown {
- display: none;
- }
- :host([loading]) .accountContainer,
- :host([logged-in]) .loginButton,
- :host([logged-in]) .registerButton {
- display: none;
- }
- :host([logged-in]) .settingsButton,
- :host([logged-in]) gr-account-dropdown {
- display: inline;
- }
- .accountContainer {
- align-items: center;
- display: flex;
- margin: 0 calc(0 - var(--spacing-m)) 0 var(--spacing-m);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .loginButton,
- .registerButton {
- padding: var(--spacing-m) var(--spacing-l);
- }
- .dropdown-trigger {
- text-decoration: none;
- }
- .dropdown-content {
- background-color: var(--view-background-color);
- box-shadow: var(--elevation-level-2);
- }
- /*
- * We are not using :host to do this, because :host has a lowest css priority
- * compared to others. This means that using :host to do this would break styles.
- */
- .linksTitle,
- .bigTitle,
- .loginButton,
- .registerButton,
- iron-icon,
- gr-account-dropdown {
- color: var(--header-text-color);
- }
- #mobileSearch {
- display: none;
- }
- @media screen and (max-width: 50em) {
- .bigTitle {
- font-family: var(--header-font-family);
- font-size: var(--font-size-h3);
- font-weight: var(--font-weight-h3);
- line-height: var(--line-height-h3);
- }
- gr-smart-search,
- .browse,
- .rightItems .hideOnMobile,
- .links > li.hideOnMobile {
- display: none;
- }
- #mobileSearch {
- display: inline-flex;
- }
- .accountContainer {
- margin-left: var(--spacing-m) !important;
- }
- gr-dropdown {
- padding: var(--spacing-m) 0 var(--spacing-m) var(--spacing-m);
- }
- }
- </style>
- <nav>
- <a href$="[[_computeRelativeURL('/')]]" class="bigTitle">
- <gr-endpoint-decorator name="header-title">
- <span class="titleText"></span>
- </gr-endpoint-decorator>
- </a>
- <ul class="links">
- <template is="dom-repeat" items="[[_links]]" as="linkGroup">
- <li class$="[[_computeLinkGroupClass(linkGroup)]]">
- <gr-dropdown
- link=""
- down-arrow=""
- items="[[linkGroup.links]]"
- horizontal-align="left"
- >
- <span class="linksTitle" id="[[linkGroup.title]]">
- [[linkGroup.title]]
- </span>
- </gr-dropdown>
- </li>
- </template>
- </ul>
- <div class="rightItems">
- <gr-endpoint-decorator
- class="hideOnMobile"
- name="header-small-banner"
- ></gr-endpoint-decorator>
- <gr-smart-search
- id="search"
- label="Search for changes"
- search-query="{{searchQuery}}"
- ></gr-smart-search>
- <gr-endpoint-decorator
- class="hideOnMobile"
- name="header-browse-source"
- ></gr-endpoint-decorator>
- <gr-endpoint-decorator class="feedbackButton" name="header-feedback">
- <template is="dom-if" if="[[_feedbackURL]]">
- <a
- href$="[[_feedbackURL]]"
- title="File a bug"
- aria-label="File a bug"
- target="_blank"
- role="button"
- >
- <iron-icon icon="gr-icons:bug"></iron-icon>
- </a>
- </template>
- </gr-endpoint-decorator>
- </div>
- <div class="accountContainer" id="accountContainer">
- <iron-icon
- id="mobileSearch"
- icon="gr-icons:search"
- on-click="_onMobileSearchTap"
- role="button"
- aria-label="[[_computeShowHideAriaLabel(mobileSearchHidden)]]"
- ></iron-icon>
- <div
- class="registerDiv"
- hidden="[[_computeRegisterHidden(_registerURL)]]"
- >
- <a class="registerButton" href$="[[_registerURL]]">
- [[_registerText]]
- </a>
- </div>
- <a class="loginButton" href$="[[loginUrl]]">Sign in</a>
- <a
- class="settingsButton"
- href$="[[_generateSettingsLink()]]"
- title="Settings"
- aria-label="Settings"
- role="button"
- >
- <iron-icon icon="gr-icons:settings"></iron-icon>
- </a>
- <template is="dom-if" if="[[_account]]">
- <gr-account-dropdown account="[[_account]]"></gr-account-dropdown>
- </template>
- </div>
- </div>
- </nav>
-`;
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
index 0f58ac0..4dcb755 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
@@ -33,29 +33,33 @@
suite('gr-main-header tests', () => {
let element: GrMainHeader;
- setup(() => {
+ setup(async () => {
stubRestApi('probePath').returns(Promise.resolve(false));
- stub('gr-main-header', '_loadAccount').callsFake(() => Promise.resolve());
+ stub('gr-main-header', 'loadAccount').callsFake(() => Promise.resolve());
element = basicFixture.instantiate();
+ await element.updateComplete;
});
- test('link visibility', () => {
+ test('link visibility', async () => {
element.loading = true;
+ await element.updateComplete;
assert.isTrue(isHidden(query(element, '.accountContainer')));
element.loading = false;
element.loggedIn = false;
+ await element.updateComplete;
assert.isFalse(isHidden(query(element, '.accountContainer')));
assert.isFalse(isHidden(query(element, '.loginButton')));
- assert.isFalse(isHidden(query(element, '.registerButton')));
- assert.isTrue(isHidden(query(element, '.registerDiv')));
+ assert.isNotOk(query(element, '.registerDiv'));
+ assert.isNotOk(query(element, '.registerButton'));
- element._account = createAccountDetailWithId(1);
- flush();
+ element.account = createAccountDetailWithId(1);
+ await element.updateComplete;
assert.isTrue(isHidden(query(element, 'gr-account-dropdown')));
assert.isTrue(isHidden(query(element, '.settingsButton')));
element.loggedIn = true;
+ await element.updateComplete;
assert.isTrue(isHidden(query(element, '.loginButton')));
assert.isTrue(isHidden(query(element, '.registerButton')));
assert.isFalse(isHidden(query(element, 'gr-account-dropdown')));
@@ -67,7 +71,7 @@
[
{url: 'https://awesometown.com/#hashyhash', name: '', target: ''},
{url: 'url', name: '', target: '_blank'},
- ].map(element._createHeaderLink),
+ ].map(element.createHeaderLink),
[
{url: 'https://awesometown.com/#hashyhash', name: ''},
{url: 'url', name: ''},
@@ -99,13 +103,13 @@
name: 'Repos',
url: '/repos',
noBaseUrl: true,
- view: null,
+ view: undefined,
},
];
// When no admin links are passed, it should use the default.
assert.deepEqual(
- element._computeLinks(
+ element.computeLinks(
/* userLinks= */ [],
adminLinks,
/* topMenus= */ [],
@@ -118,7 +122,7 @@
})
);
assert.deepEqual(
- element._computeLinks(
+ element.computeLinks(
userLinks,
adminLinks,
/* topMenus= */ [],
@@ -146,11 +150,11 @@
},
];
- assert.deepEqual(element._getDocLinks(null, docLinks), []);
- assert.deepEqual(element._getDocLinks('', docLinks), []);
- assert.deepEqual(element._getDocLinks('base', []), []);
+ assert.deepEqual(element.getDocLinks(null, docLinks), []);
+ assert.deepEqual(element.getDocLinks('', docLinks), []);
+ assert.deepEqual(element.getDocLinks('base', []), []);
- assert.deepEqual(element._getDocLinks('base', docLinks), [
+ assert.deepEqual(element.getDocLinks('base', docLinks), [
{
name: 'Table of Contents',
target: '_blank',
@@ -158,7 +162,7 @@
},
]);
- assert.deepEqual(element._getDocLinks('base/', docLinks), [
+ assert.deepEqual(element.getDocLinks('base/', docLinks), [
{
name: 'Table of Contents',
target: '_blank',
@@ -173,7 +177,7 @@
name: 'Repos',
url: '/repos',
noBaseUrl: true,
- view: null,
+ view: undefined,
},
];
const topMenus = [
@@ -189,7 +193,7 @@
},
];
assert.deepEqual(
- element._computeLinks(
+ element.computeLinks(
/* userLinks= */ [],
adminLinks,
topMenus,
@@ -220,7 +224,7 @@
name: 'Repos',
url: '/repos',
noBaseUrl: true,
- view: null,
+ view: undefined,
},
];
const topMenus = [
@@ -241,7 +245,7 @@
},
];
assert.deepEqual(
- element._computeLinks(
+ element.computeLinks(
/* userLinks= */ [],
adminLinks,
topMenus,
@@ -272,7 +276,7 @@
name: 'Repos',
url: '/repos',
noBaseUrl: true,
- view: null,
+ view: undefined,
},
];
const topMenus = [
@@ -298,7 +302,7 @@
},
];
assert.deepEqual(
- element._computeLinks(
+ element.computeLinks(
/* userLinks= */ [],
adminLinks,
topMenus,
@@ -352,7 +356,7 @@
},
];
assert.deepEqual(
- element._computeLinks(
+ element.computeLinks(
/* userLinks= */ [],
/* adminLinks= */ [],
topMenus,
@@ -398,7 +402,7 @@
},
];
assert.deepEqual(
- element._computeLinks(
+ element.computeLinks(
userLinks,
/* adminLinks= */ [],
topMenus,
@@ -434,7 +438,7 @@
name: 'Repos',
url: '/repos',
noBaseUrl: true,
- view: null,
+ view: undefined,
},
];
const topMenus = [
@@ -450,7 +454,7 @@
},
];
assert.deepEqual(
- element._computeLinks(
+ element.computeLinks(
/* userLinks= */ [],
adminLinks,
topMenus,
@@ -473,7 +477,7 @@
});
test('shows feedback icon when URL provided', async () => {
- assert.isEmpty(element._feedbackURL);
+ assert.isEmpty(element.feedbackURL);
assert.isNotOk(query(element, '.feedbackButton > a'));
const url = 'report_bug_url';
@@ -484,14 +488,14 @@
report_bug_url: url,
},
};
- element._retrieveFeedbackURL(config);
- await flush();
+ element.retrieveFeedbackURL(config);
+ await element.updateComplete;
- assert.equal(element._feedbackURL, url);
+ assert.equal(element.feedbackURL, url);
assert.ok(query(element, '.feedbackButton > a'));
});
- test('register URL', () => {
+ test('register URL', async () => {
assert.isTrue(isHidden(query(element, '.registerDiv')));
const config: ServerInfo = {
...createServerInfo(),
@@ -501,19 +505,21 @@
editable_account_fields: [],
},
};
- element._retrieveRegisterURL(config);
- assert.equal(element._registerURL, config.auth.register_url);
- assert.equal(element._registerText, 'Sign up');
+ element.retrieveRegisterURL(config);
+ await element.updateComplete;
+ assert.equal(element.registerURL, config.auth.register_url);
+ assert.equal(element.registerText, 'Sign up');
assert.isFalse(isHidden(query(element, '.registerDiv')));
config.auth.register_text = 'Create account';
- element._retrieveRegisterURL(config);
- assert.equal(element._registerURL, config.auth.register_url);
- assert.equal(element._registerText, config.auth.register_text);
+ element.retrieveRegisterURL(config);
+ await element.updateComplete;
+ assert.equal(element.registerURL, config.auth.register_url);
+ assert.equal(element.registerText, config.auth.register_text);
assert.isFalse(isHidden(query(element, '.registerDiv')));
});
- test('register URL ignored for wrong auth type', () => {
+ test('register URL ignored for wrong auth type', async () => {
const config: ServerInfo = {
...createServerInfo(),
auth: {
@@ -522,9 +528,10 @@
editable_account_fields: [],
},
};
- element._retrieveRegisterURL(config);
- assert.equal(element._registerURL, '');
- assert.equal(element._registerText, 'Sign up');
+ element.retrieveRegisterURL(config);
+ await element.updateComplete;
+ assert.equal(element.registerURL, '');
+ assert.equal(element.registerText, 'Sign up');
assert.isTrue(isHidden(query(element, '.registerDiv')));
});
});
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
index b8f2630..cfc424d 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
@@ -40,65 +40,7 @@
//
// Each object has a `view` property with a value from GerritNav.View. The
// remaining properties depend on the value used for view.
-//
-// - GerritNav.View.CHANGE:
-// - `changeNum`, required, String: the numeric ID of the change.
-// - `project`, optional, String: the project name.
-// - `patchNum`, optional, Number: the patch for the right-hand-side of
-// the diff.
-// - `basePatchNum`, optional, Number: the patch for the left-hand-side
-// of the diff. If `basePatchNum` is provided, then `patchNum` must
-// also be provided.
-// - `edit`, optional, Boolean: whether or not to load the file list with
-// edit controls.
-// - `messageHash`, optional, String: the hash of the change message to
-// scroll to.
-//
-// - GerritNav.View.SEARCH:
-// - `query`, optional, String: the literal search query. If provided,
-// the string will be used as the query, and all other params will be
-// ignored.
-// - `owner`, optional, String: the owner name.
-// - `project`, optional, String: the project name.
-// - `branch`, optional, String: the branch name.
-// - `topic`, optional, String: the topic name.
-// - `hashtag`, optional, String: the hashtag name.
-// - `statuses`, optional, Array<String>: the list of change statuses to
-// search for. If more than one is provided, the search will OR them
-// together.
-// - `offset`, optional, Number: the offset for the query.
-//
-// - GerritNav.View.DIFF:
-// - `changeNum`, required, String: the numeric ID of the change.
-// - `path`, required, String: the filepath of the diff.
-// - `patchNum`, required, Number: the patch for the right-hand-side of
-// the diff.
-// - `basePatchNum`, optional, Number: the patch for the left-hand-side
-// of the diff. If `basePatchNum` is provided, then `patchNum` must
-// also be provided.
-// - `lineNum`, optional, Number: the line number to be selected on load.
-// - `leftSide`, optional, Boolean: if a `lineNum` is provided, a value
-// of true selects the line from base of the patch range. False by
-// default.
-//
-// - GerritNav.View.GROUP:
-// - `groupId`, required, String: the ID of the group.
-// - `detail`, optional, String: the name of the group detail view.
-// Takes any value from GerritNav.GroupDetailView.
-//
-// - GerritNav.View.REPO:
-// - `repoName`, required, String: the name of the repo
-// - `detail`, optional, String: the name of the repo detail view.
-// Takes any value from GerritNav.RepoDetailView.
-//
-// - GerritNav.View.DASHBOARD
-// - `repo`, optional, String.
-// - `sections`, optional, Array of objects with `title` and `query`
-// strings.
-// - `user`, optional, String.
-//
-// - GerritNav.View.ROOT:
-// - no possible parameters.
+// GenerateUrlParameters lists all the possible view parameters.
const uninitialized = () => {
console.warn('Use of uninitialized routing');
@@ -132,7 +74,6 @@
suffixForDashboard?: string;
selfOnly?: boolean;
hideIfEmpty?: boolean;
- assigneeOnly?: boolean;
isOutgoing?: boolean;
results?: ChangeInfo[];
}
@@ -165,16 +106,6 @@
hideIfEmpty: false,
suffixForDashboard: 'limit:25',
};
-const ASSIGNED: DashboardSection = {
- // Changes that are assigned to the viewed user.
- name: 'Assigned reviews',
- query:
- 'assignee:${user} (-is:wip OR owner:self OR assignee:self) ' +
- 'is:open -is:ignored',
- hideIfEmpty: true,
- suffixForDashboard: 'limit:25',
- assigneeOnly: true,
-};
const WIP: DashboardSection = {
// WIP open changes owned by viewing user. This section is omitted when
// viewing other users, so we don't need to filter anything out.
@@ -194,12 +125,10 @@
};
const INCOMING: DashboardSection = {
// Non-WIP open changes not owned by the viewed user, that the viewed user
- // is associated with (as either a reviewer or the assignee). Changes
- // ignored by the viewing user are filtered out.
+ // is associated with as a reviewer. Changes ignored by the viewing user are
+ // filtered out.
name: 'Incoming reviews',
- query:
- 'is:open -owner:${user} -is:wip -is:ignored ' +
- '(reviewer:${user} OR assignee:${user})',
+ query: 'is:open -owner:${user} -is:wip -is:ignored reviewer:${user}',
suffixForDashboard: 'limit:25',
};
const CCED: DashboardSection = {
@@ -211,20 +140,18 @@
};
export const CLOSED: DashboardSection = {
name: 'Recently closed',
- // Closed changes where viewed user is owner, reviewer, or assignee.
+ // Closed changes where viewed user is owner or reviewer.
// Changes ignored by the viewing user are filtered out, and so are WIP
// changes not owned by the viewing user (the one instance of
// 'owner:self' is intentional and implements this logic).
query:
'is:closed -is:ignored (-is:wip OR owner:self) ' +
- '(owner:${user} OR reviewer:${user} OR assignee:${user} ' +
- 'OR cc:${user})',
+ '(owner:${user} OR reviewer:${user} OR cc:${user})',
suffixForDashboard: '-age:4w limit:10',
};
const DEFAULT_SECTIONS: DashboardSection[] = [
HAS_DRAFTS,
YOUR_TURN,
- ASSIGNED,
WIP,
OUTGOING,
INCOMING,
@@ -256,11 +183,9 @@
edit?: boolean;
host?: string;
messageHash?: string;
- queryMap?: Map<string, string> | URLSearchParams;
commentId?: UrlEncodedCommentId;
-
- // TODO(TS): querystring isn't set anywhere, try to remove
- querystring?: string;
+ forceReload?: boolean;
+ tab?: string;
}
export interface GenerateUrlRepoViewParameters {
@@ -318,11 +243,17 @@
commentLink?: boolean;
}
+export interface GenerateUrlTopicViewParams {
+ view: GerritView.TOPIC;
+ topic?: string;
+}
+
export type GenerateUrlParameters =
| GenerateUrlSearchViewParameters
| GenerateUrlChangeViewParameters
| GenerateUrlRepoViewParameters
| GenerateUrlDashboardViewParameters
+ | GenerateUrlTopicViewParams
| GenerateUrlGroupViewParameters
| GenerateUrlEditViewParameters
| GenerateUrlRootViewParameters
@@ -435,6 +366,18 @@
RESOLVE_CONFLICTS = 'resolve-conflicts',
}
+interface NavigateToChangeParams {
+ patchNum?: PatchSetNum;
+ basePatchNum?: BasePatchSetNum;
+ isEdit?: boolean;
+ redirect?: boolean;
+ forceReload?: boolean;
+}
+
+interface ChangeUrlParams extends NavigateToChangeParams {
+ messageHash?: string;
+}
+
// TODO(dmfilippov) Convert to class, extract consts, give better name and
// expose as a service from appContext
export const GerritNav = {
@@ -609,11 +552,9 @@
*/
getUrlForChange(
change: Pick<ChangeInfo, '_number' | 'project' | 'internalHost'>,
- patchNum?: PatchSetNum,
- basePatchNum?: BasePatchSetNum,
- isEdit?: boolean,
- messageHash?: string
+ options: ChangeUrlParams = {}
) {
+ let {patchNum, basePatchNum, isEdit, messageHash, forceReload} = options;
if (basePatchNum === ParentPatchSetNum) {
basePatchNum = undefined;
}
@@ -628,6 +569,7 @@
edit: isEdit,
host: change.internalHost || undefined,
messageHash,
+ forceReload,
});
},
@@ -649,21 +591,36 @@
* @param redirect redirect to a change - if true, the current
* location (i.e. page which makes redirect) is not added to a history.
* I.e. back/forward buttons skip current location
- *
+ * @param forceReload Some views are smart about how to handle the reload
+ * of the view. In certain cases we want to force the view to reload
+ * and re-render everything.
*/
navigateToChange(
change: Pick<ChangeInfo, '_number' | 'project' | 'internalHost'>,
- patchNum?: PatchSetNum,
- basePatchNum?: BasePatchSetNum,
- isEdit?: boolean,
- redirect?: boolean
+ options: NavigateToChangeParams = {}
) {
+ const {patchNum, basePatchNum, isEdit, forceReload, redirect} = options;
this._navigate(
- this.getUrlForChange(change, patchNum, basePatchNum, isEdit),
+ this.getUrlForChange(change, {
+ patchNum,
+ basePatchNum,
+ isEdit,
+ forceReload,
+ }),
redirect
);
},
+ navigateToTopicPage(topic: string) {
+ this._navigate(
+ this._getUrlFor({
+ view: GerritView.TOPIC,
+ topic,
+ }),
+ true /* redirect */
+ );
+ },
+
/**
* @param basePatchNum The string 'PARENT' can be used for none.
*/
@@ -1016,12 +973,9 @@
getUserDashboard(
user = 'self',
sections = DEFAULT_SECTIONS,
- title = '',
- config: UserDashboardConfig = {}
+ title = ''
): UserDashboard {
- const assigneeEnabled = config.change && !!config.change.enable_assignee;
sections = sections
- .filter(section => assigneeEnabled || !section.assigneeOnly)
.filter(section => user === 'self' || !section.selfOnly)
.map(section => {
return {
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.js b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.js
index 93a1e9e..40a06bc 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.js
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.js
@@ -20,7 +20,7 @@
suite('gr-navigation tests', () => {
test('invalid patch ranges throw exceptions', () => {
- assert.throw(() => GerritNav.getUrlForChange('123', undefined, 12));
+ assert.throw(() => GerritNav.getUrlForChange('123', {basePatchNum: 12}));
assert.throw(() => GerritNav.getUrlForDiff('123', 'x.c', undefined, 12));
});
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 f0cff85..2a35494 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -43,8 +43,9 @@
isGenerateUrlDiffViewParameters,
RepoDetailView,
WeblinkType,
+ GenerateUrlTopicViewParams,
} from '../gr-navigation/gr-navigation';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {convertToPatchSetNum} from '../../../utils/patch-set-util';
import {customElement, property} from '@polymer/decorators';
import {assertNever} from '../../../utils/common-util';
@@ -65,7 +66,7 @@
AppElementParams,
} from '../../gr-app-types';
import {LocationChangeEventDetail} from '../../../types/events';
-import {GerritView, updateState} from '../../../services/router/router-model';
+import {GerritView} from '../../../services/router/router-model';
import {firePageError} from '../../../utils/event-util';
import {addQuotesWhen} from '../../../utils/string-util';
import {windowLocationReload} from '../../../utils/dom-util';
@@ -77,11 +78,14 @@
toSearchParams,
} from '../../../utils/url-util';
import {Execution, LifeCycle, Timing} from '../../../constants/reporting';
+import {KnownExperimentId} from '../../../services/flags/flags';
const RoutePattern = {
ROOT: '/',
DASHBOARD: /^\/dashboard\/(.+)$/,
+ // TODO(dhruvsri): remove /c once Change 322894 lands
+ TOPIC: /^\/c\/topic\/([^/]*)\/?$/,
CUSTOM_DASHBOARD: /^\/dashboard\/?$/,
PROJECT_DASHBOARD: /^\/p\/(.+)\/\+\/dashboard\/(.+)/,
LEGACY_PROJECT_DASHBOARD: /^\/projects\/(.+),dashboards\/(.+)/,
@@ -273,7 +277,7 @@
// Setup listeners outside of the router component initialization.
(function () {
window.addEventListener('WebComponentsReady', () => {
- appContext.reportingService.timeEnd(Timing.WEB_COMPONENTS_READY);
+ getAppContext().reportingService.timeEnd(Timing.WEB_COMPONENTS_READY);
});
})();
@@ -305,9 +309,13 @@
@property({type: Boolean})
_isInitialLoad = true;
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
- private readonly restApiService = appContext.restApiService;
+ private readonly routerModel = getAppContext().routerModel;
+
+ private readonly restApiService = getAppContext().restApiService;
+
+ private readonly flagsService = getAppContext().flagsService;
start() {
if (!this._app) {
@@ -317,11 +325,11 @@
}
_setParams(params: AppElementParams | GenerateUrlParameters) {
- updateState(
- params.view,
- 'changeNum' in params ? params.changeNum : undefined,
- 'patchNum' in params ? params.patchNum ?? undefined : undefined
- );
+ this.routerModel.updateState({
+ view: params.view,
+ changeNum: 'changeNum' in params ? params.changeNum : undefined,
+ patchNum: 'patchNum' in params ? params.patchNum ?? undefined : undefined,
+ });
this._appElement().params = params;
}
@@ -355,6 +363,8 @@
url = this._generateChangeUrl(params);
} else if (params.view === GerritView.DASHBOARD) {
url = this._generateDashboardUrl(params);
+ } else if (params.view === GerritView.TOPIC) {
+ url = this._generateTopicPageUrl(params);
} else if (
params.view === GerritView.DIFF ||
params.view === GerritView.EDIT
@@ -530,17 +540,22 @@
range = '/' + range;
}
let suffix = `${range}`;
- if (params.querystring) {
- suffix += '?' + params.querystring;
- } else if (params.edit) {
- suffix += ',edit';
+ let queryString = '';
+ if (params.forceReload) {
+ queryString = 'forceReload=true';
}
- if (params.messageHash) {
- suffix += params.messageHash;
+ if (params.edit) {
+ suffix += ',edit';
}
if (params.commentId) {
suffix = suffix + `/comments/${params.commentId}`;
}
+ if (queryString) {
+ suffix += '?' + queryString;
+ }
+ if (params.messageHash) {
+ suffix += params.messageHash;
+ }
if (params.project) {
const encodedProject = encodeURL(params.project, true);
return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
@@ -572,6 +587,10 @@
}
}
+ _generateTopicPageUrl(params: GenerateUrlTopicViewParams) {
+ return `/c/topic/${params.topic ?? ''}`;
+ }
+
_sectionsToEncodedParams(sections: DashboardSection[], repoName?: RepoName) {
return sections.map(section => {
// If there is a repo name provided, make sure to substitute it into the
@@ -888,6 +907,8 @@
this._mapRoute(RoutePattern.DASHBOARD, '_handleDashboardRoute');
+ this._mapRoute(RoutePattern.TOPIC, '_handleTopicRoute');
+
this._mapRoute(
RoutePattern.CUSTOM_DASHBOARD,
'_handleCustomDashboardRoute'
@@ -1212,6 +1233,13 @@
});
}
+ _handleTopicRoute(data: PageContextWithQueryMap) {
+ this._setParams({
+ view: GerritView.TOPIC,
+ topic: data.params[0],
+ });
+ }
+
/**
* Handle custom dashboard routes.
*
@@ -1529,6 +1557,16 @@
}
_handleQueryRoute(data: PageContextWithQueryMap) {
+ if (this.flagsService.isEnabled(KnownExperimentId.TOPICS_PAGE)) {
+ const query = data.params[0];
+ const terms = query.split(' ');
+ if (terms.length === 1) {
+ const tokens = terms[0].split(':');
+ if (tokens[0] === 'topic') {
+ return GerritNav.navigateToTopicPage(tokens[1]);
+ }
+ }
+ }
this._setParams({
view: GerritView.SEARCH,
query: data.params[0],
@@ -1563,9 +1601,20 @@
basePatchNum: convertToPatchSetNum(ctx.params[4]) as BasePatchSetNum,
patchNum: convertToPatchSetNum(ctx.params[6]),
view: GerritView.CHANGE,
- queryMap: ctx.queryMap,
};
+ if (ctx.queryMap.has('forceReload')) {
+ params.forceReload = true;
+ history.replaceState(
+ null,
+ '',
+ location.href.replace(/[?&]forceReload=true/, '')
+ );
+ }
+
+ const tab = ctx.queryMap.get('tab');
+ if (tab) params.tab = tab;
+
this.reporting.setRepoName(params.project);
this.reporting.setChangeId(changeNum);
this._redirectOrNavigate(params);
@@ -1661,13 +1710,24 @@
// Parameter order is based on the regex group number matched.
const project = ctx.params[0] as RepoName;
const changeNum = Number(ctx.params[1]) as NumericChangeId;
- this._redirectOrNavigate({
+ const params: GenerateUrlChangeViewParameters = {
project,
changeNum,
patchNum: convertToPatchSetNum(ctx.params[3]),
view: GerritView.CHANGE,
edit: true,
- });
+ tab: ctx.queryMap.get('tab') ?? '',
+ };
+ if (ctx.queryMap.has('forceReload')) {
+ params.forceReload = true;
+ history.replaceState(
+ null,
+ '',
+ location.href.replace(/[?&]forceReload=true/, '')
+ );
+ }
+ this._redirectOrNavigate(params);
+
this.reporting.setRepoName(project);
this.reporting.setChangeId(changeNum);
}
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
index b91bf0c..a7de155 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
@@ -19,10 +19,11 @@
import './gr-router.js';
import {page} from '../../../utils/page-wrapper-utils.js';
import {GerritNav} from '../gr-navigation/gr-navigation.js';
-import {stubBaseUrl, stubRestApi, addListenerForTest} from '../../../test/test-utils.js';
+import {stubBaseUrl, stubRestApi, addListenerForTest, stubFlags} from '../../../test/test-utils.js';
import {_testOnly_RoutePattern} from './gr-router.js';
import {GerritView} from '../../../services/router/router-model.js';
import {ParentPatchSetNum} from '../../../types/common.js';
+import {KnownExperimentId} from '../../../services/flags/flags.js';
const basicFixture = fixtureFromElement('gr-router');
@@ -214,6 +215,7 @@
'_handleTagListFilterOffsetRoute',
'_handleTagListFilterRoute',
'_handleTagListOffsetRoute',
+ '_handleTopicRoute',
'_handlePluginScreen',
];
@@ -259,6 +261,15 @@
});
suite('generateUrl', () => {
+ test('topic page', () => {
+ const params = {
+ view: GerritView.TOPIC,
+ topic: 'ggh',
+ };
+ assert.equal(element._generateUrl(params),
+ '/c/topic/ggh');
+ });
+
test('search', () => {
let params = {
view: GerritNav.View.SEARCH,
@@ -312,28 +323,14 @@
changeNum: '1234',
project: 'test',
};
- const paramsWithQuery = {
- view: GerritView.CHANGE,
- changeNum: '1234',
- project: 'test',
- querystring: 'revert&foo=bar',
- };
assert.equal(element._generateUrl(params), '/c/test/+/1234');
- assert.equal(element._generateUrl(paramsWithQuery),
- '/c/test/+/1234?revert&foo=bar');
params.patchNum = 10;
assert.equal(element._generateUrl(params), '/c/test/+/1234/10');
- paramsWithQuery.patchNum = 10;
- assert.equal(element._generateUrl(paramsWithQuery),
- '/c/test/+/1234/10?revert&foo=bar');
params.basePatchNum = 5;
assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10');
- paramsWithQuery.basePatchNum = 5;
- assert.equal(element._generateUrl(paramsWithQuery),
- '/c/test/+/1234/5..10?revert&foo=bar');
params.messageHash = '#123';
assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10#123');
@@ -678,28 +675,27 @@
});
});
+ test('_handleQueryRoute to topic page', () => {
+ stubFlags('isEnabled').withArgs(KnownExperimentId.TOPICS_PAGE)
+ .returns(true);
+ const navStub = sinon.stub(GerritNav, 'navigateToTopicPage');
+ let data = {params: ['topic:abcd']};
+ element._handleQueryRoute(data);
+
+ assert.isTrue(navStub.called);
+
+ // multiple terms so topic page is not loaded
+ data = {params: ['topic:abcd owner:self']};
+ element._handleQueryRoute(data);
+ assert.isTrue(navStub.calledOnce);
+ });
+
test('_handleQueryLegacySuffixRoute', () => {
element._handleQueryLegacySuffixRoute({path: '/q/foo+bar,n,z'});
assert.isTrue(redirectStub.calledOnce);
assert.equal(redirectStub.lastCall.args[0], '/q/foo+bar');
});
- test('_handleQueryRoute', () => {
- const data = {params: ['project:foo/bar/baz']};
- assertDataToParams(data, '_handleQueryRoute', {
- view: GerritNav.View.SEARCH,
- query: 'project:foo/bar/baz',
- offset: undefined,
- });
-
- data.params.push(',123', '123');
- assertDataToParams(data, '_handleQueryRoute', {
- view: GerritNav.View.SEARCH,
- query: 'project:foo/bar/baz',
- offset: '123',
- });
- });
-
test('_handleChangeIdQueryRoute', () => {
const data = {params: ['I0123456789abcdef0123456789abcdef01234567']};
assertDataToParams(data, '_handleChangeIdQueryRoute', {
@@ -1244,6 +1240,19 @@
});
});
+ suite('topic routes', () => {
+ test('_handleTopicRoute', () => {
+ const url = '/c/topic/super complex-topic name with spaces/';
+ const groups = url.match(_testOnly_RoutePattern.TOPIC);
+
+ const data = {params: groups.slice(1)};
+ assertDataToParams(data, '_handleTopicRoute', {
+ view: GerritView.TOPIC,
+ topic: 'super complex-topic name with spaces',
+ });
+ });
+ });
+
suite('plugin routes', () => {
test('_handlePluginListOffsetRoute', () => {
const data = {params: {}};
@@ -1382,7 +1391,6 @@
changeNum: 1234,
basePatchNum: 4,
patchNum: 7,
- queryMap: new Map(),
});
assert.isFalse(redirectStub.called);
assert.isTrue(normalizeRangeStub.called);
@@ -1549,6 +1557,7 @@
null,
3, // 3 Patch num
],
+ queryMap: new Map(),
};
const appParams = {
project: 'foo/bar',
@@ -1556,6 +1565,7 @@
view: GerritView.CHANGE,
patchNum: 3,
edit: true,
+ tab: '',
};
element._handleChangeEditRoute(ctx);
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
index 2901b8a..7bbb93b 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
@@ -15,16 +15,6 @@
* limitations under the License.
*/
import '../../shared/gr-autocomplete/gr-autocomplete';
-import '../../../styles/shared-styles';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-search-bar_html';
-import {
- KeyboardShortcutMixin,
- Shortcut,
- ShortcutListener,
-} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
-import {customElement, property} from '@polymer/decorators';
import {ServerInfo} from '../../../types/common';
import {
AutocompleteQuery,
@@ -33,8 +23,19 @@
} from '../../shared/gr-autocomplete/gr-autocomplete';
import {getDocsBaseUrl} from '../../../utils/url-util';
import {MergeabilityComputationBehavior} from '../../../constants/constants';
-import {appContext} from '../../../services/app-context';
-import {listen} from '../../../services/shortcuts/shortcuts-service';
+import {getAppContext} from '../../../services/app-context';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, html, css} from 'lit';
+import {
+ customElement,
+ property,
+ state,
+ query as queryDec,
+} from 'lit/decorators';
+import {ShortcutController} from '../../lit/shortcut-controller';
+import {query as queryUtil} from '../../../utils/common-util';
+import {assertIsDefined} from '../../../utils/common-util';
+import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
// Possible static search options for auto complete, without negations.
const SEARCH_OPERATORS: ReadonlyArray<string> = [
@@ -42,7 +43,6 @@
'after:',
'age:',
'age:1week', // Give an example age
- 'assignee:',
'attention:',
'author:',
'before:',
@@ -76,16 +76,15 @@
'intopic:',
'is:',
'is:abandoned',
- 'is:assigned',
'is:attention',
'is:cherrypick',
'is:closed',
- 'is:ignored',
'is:merge',
'is:merged',
'is:open',
'is:owner',
'is:private',
+ 'is:pure-revert',
'is:reviewed',
'is:reviewer',
'is:starred',
@@ -143,34 +142,18 @@
inputVal: string;
}
-export interface GrSearchBar {
- $: {
- searchInput: GrAutocomplete;
- };
-}
-
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
-
@customElement('gr-search-bar')
-export class GrSearchBar extends base {
- static get template() {
- return htmlTemplate;
- }
-
- private searchOperators = new Set(SEARCH_OPERATORS_WITH_NEGATIONS_SET);
-
+export class GrSearchBar extends LitElement {
/**
* Fired when a search is committed
*
* @event handle-search
*/
- @property({type: String, notify: true, observer: '_valueChanged'})
- value = '';
+ @queryDec('#searchInput') protected searchInput?: GrAutocomplete;
- @property({type: Object})
- query: AutocompleteQuery;
+ @property({type: String})
+ value = '';
@property({type: Object})
projectSuggestions: SuggestionProvider = () => Promise.resolve([]);
@@ -181,76 +164,140 @@
@property({type: Object})
accountSuggestions: SuggestionProvider = () => Promise.resolve([]);
- @property({type: String})
- _inputVal = '';
-
- @property({type: Number})
- _threshold = 1;
+ @property({type: Object})
+ serverConfig?: ServerInfo;
@property({type: String})
label = '';
- @property({type: String})
- docBaseUrl: string | null = null;
+ // private but used in test
+ @state() inputVal = '';
- private readonly restApiService = appContext.restApiService;
+ // private but used in test
+ @state() docBaseUrl: string | null = null;
+
+ @state() private query: AutocompleteQuery;
+
+ @state() private threshold = 1;
+
+ private searchOperators = new Set(SEARCH_OPERATORS_WITH_NEGATIONS_SET);
+
+ private readonly restApiService = getAppContext().restApiService;
+
+ private readonly shortcuts = new ShortcutController(this);
constructor() {
super();
- this.query = (input: string) => this._getSearchSuggestions(input);
+ this.query = (input: string) => this.getSearchSuggestions(input);
+ this.shortcuts.addAbstract(Shortcut.SEARCH, () => this.handleSearch());
}
- override connectedCallback() {
- super.connectedCallback();
- this.restApiService.getConfig().then((serverConfig?: ServerInfo) => {
- const mergeability =
- serverConfig &&
- serverConfig.change &&
- serverConfig.change.mergeability_computation_behavior;
- if (
- mergeability ===
- MergeabilityComputationBehavior.API_REF_UPDATED_AND_CHANGE_REINDEX ||
- mergeability ===
- MergeabilityComputationBehavior.REF_UPDATED_AND_CHANGE_REINDEX
- ) {
- // add 'is:mergeable' to searchOperators
- this._addOperator('is:mergeable');
- }
- if (serverConfig) {
- getDocsBaseUrl(serverConfig, this.restApiService).then(baseUrl => {
- this.docBaseUrl = baseUrl;
- });
- }
- });
+ static override get styles() {
+ return [
+ sharedStyles,
+ css`
+ form {
+ display: flex;
+ }
+ gr-autocomplete {
+ background-color: var(--view-background-color);
+ border-radius: var(--border-radius);
+ flex: 1;
+ outline: none;
+ }
+ `,
+ ];
}
- _computeHelpDocLink(docBaseUrl: string | null) {
+ override render() {
+ return html`
+ <form>
+ <gr-autocomplete
+ id="searchInput"
+ .label=${this.label}
+ show-search-icon
+ .text=${this.inputVal}
+ .query=${this.query}
+ allow-non-suggested-values
+ multi
+ .threshold=${this.threshold}
+ tab-complete
+ .verticalOffset=${30}
+ @commit=${(e: Event) => {
+ this.handleInputCommit(e);
+ }}
+ @text-changed=${(e: CustomEvent) => {
+ this.handleSearchTextChanged(e);
+ }}
+ >
+ <a
+ class="help"
+ slot="suffix"
+ href=${this.computeHelpDocLink()}
+ target="_blank"
+ tabindex="-1"
+ >
+ <iron-icon
+ icon="gr-icons:help-outline"
+ title="read documentation"
+ ></iron-icon>
+ </a>
+ </gr-autocomplete>
+ </form>
+ `;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('serverConfig')) {
+ this.serverConfigChanged();
+ }
+
+ if (changedProperties.has('value')) {
+ this.valueChanged();
+ }
+ }
+
+ private serverConfigChanged() {
+ const mergeability =
+ this.serverConfig?.change?.mergeability_computation_behavior;
+ if (
+ mergeability ===
+ MergeabilityComputationBehavior.API_REF_UPDATED_AND_CHANGE_REINDEX ||
+ mergeability ===
+ MergeabilityComputationBehavior.REF_UPDATED_AND_CHANGE_REINDEX
+ ) {
+ // add 'is:mergeable' to searchOperators
+ this.searchOperators.add('is:mergeable');
+ this.searchOperators.add('-is:mergeable');
+ } else {
+ this.searchOperators.delete('is:mergeable');
+ this.searchOperators.delete('-is:mergeable');
+ }
+ if (this.serverConfig) {
+ getDocsBaseUrl(this.serverConfig, this.restApiService).then(baseUrl => {
+ this.docBaseUrl = baseUrl;
+ });
+ }
+ }
+
+ private valueChanged() {
+ this.inputVal = this.value;
+ }
+
+ // private but used in test
+ computeHelpDocLink() {
// fallback to gerrit's official doc
let baseUrl =
- docBaseUrl || 'https://gerrit-review.googlesource.com/documentation/';
+ this.docBaseUrl ||
+ 'https://gerrit-review.googlesource.com/documentation/';
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.substring(0, baseUrl.length - 1);
}
return `${baseUrl}/user-search.html`;
}
- _addOperator(name: string, include_neg = true) {
- this.searchOperators.add(name);
- if (include_neg) {
- this.searchOperators.add(`-${name}`);
- }
- }
-
- override keyboardShortcuts(): ShortcutListener[] {
- return [listen(Shortcut.SEARCH, _ => this._handleSearch())];
- }
-
- _valueChanged(value: string) {
- this._inputVal = value;
- }
-
- _handleInputCommit(e: Event) {
- this._preventDefaultAndNavigateToInputVal(e);
+ private handleInputCommit(e: Event) {
+ this.preventDefaultAndNavigateToInputVal(e);
}
/**
@@ -259,18 +306,18 @@
* - e.target is the gr-autocomplete widget (#searchInput)
* - e.target is the input element wrapped within #searchInput
*/
- _preventDefaultAndNavigateToInputVal(e: Event) {
+ private preventDefaultAndNavigateToInputVal(e: Event) {
e.preventDefault();
- const target = (dom(e) as EventApi).rootTarget as PolymerElement;
+ const target = e.composedPath()[0] as HTMLElement;
// If the target is the #searchInput or has a sub-input component, that
// is what holds the focus as opposed to the target from the DOM event.
- if (target.$['input']) {
- (target.$['input'] as HTMLElement).blur();
+ if (queryUtil(target, '#input')) {
+ queryUtil<HTMLElement>(target, '#input')!.blur();
} else {
target.blur();
}
- if (!this._inputVal) return;
- const trimmedInput = this._inputVal.trim();
+ if (!this.inputVal) return;
+ const trimmedInput = this.inputVal.trim();
if (trimmedInput) {
const predefinedOpOnlyQuery = [...this.searchOperators].some(
op => op.endsWith(':') && op === trimmedInput
@@ -279,7 +326,7 @@
return;
}
const detail: SearchBarHandleSearchDetail = {
- inputVal: this._inputVal,
+ inputVal: this.inputVal,
};
this.dispatchEvent(
new CustomEvent('handle-search', {
@@ -291,13 +338,13 @@
/**
* Determine what array of possible suggestions should be provided
- * to _getSearchSuggestions.
+ * to getSearchSuggestions.
*
* @param input - The full search term, in lowercase.
* @return This returns a promise that resolves to an array of
* suggestion objects.
*/
- _fetchSuggestions(input: string): Promise<AutocompleteSuggestion[]> {
+ private fetchSuggestions(input: string): Promise<AutocompleteSuggestion[]> {
// Split the input on colon to get a two part predicate/expression.
const splitInput = input.split(':');
const predicate = splitInput[0];
@@ -315,7 +362,6 @@
// Fetch projects.
return this.projectSuggestions(predicate, expression);
- case 'assignee':
case 'attention':
case 'author':
case 'cc':
@@ -345,14 +391,16 @@
* @param input - The complete search query.
* @return This returns a promise that resolves to an array of
* suggestions.
+ *
+ * private but used in test
*/
- _getSearchSuggestions(input: string): Promise<AutocompleteSuggestion[]> {
+ getSearchSuggestions(input: string): Promise<AutocompleteSuggestion[]> {
// Allow spaces within quoted terms.
const tokens = input.match(TOKENIZE_REGEX);
if (tokens === null) return Promise.resolve([]);
const trimmedInput = tokens[tokens.length - 1].toLowerCase();
- return this._fetchSuggestions(trimmedInput).then(suggestions => {
+ return this.fetchSuggestions(trimmedInput).then(suggestions => {
if (!suggestions || !suggestions.length) {
return [];
}
@@ -390,9 +438,14 @@
});
}
- _handleSearch() {
- this.$.searchInput.focus();
- this.$.searchInput.selectAll();
+ private handleSearch() {
+ assertIsDefined(this.searchInput, 'searchInput');
+ this.searchInput.focus();
+ this.searchInput.selectAll();
+ }
+
+ private handleSearchTextChanged(e: CustomEvent) {
+ this.inputVal = e.detail.value;
}
}
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.ts
deleted file mode 100644
index a0de7f2..0000000
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- form {
- display: flex;
- }
- gr-autocomplete {
- background-color: var(--view-background-color);
- border-radius: var(--border-radius);
- flex: 1;
- outline: none;
- }
- </style>
- <form>
- <gr-autocomplete
- label="[[label]]"
- show-search-icon=""
- id="searchInput"
- text="{{_inputVal}}"
- query="[[query]]"
- on-commit="_handleInputCommit"
- allow-non-suggested-values=""
- multi=""
- threshold="[[_threshold]]"
- tab-complete=""
- vertical-offset="30"
- >
- <a
- slot="suffix"
- href$="[[_computeHelpDocLink(docBaseUrl)]]"
- target="_blank"
- class="help"
- tabindex="-1"
- >
- <iron-icon
- icon="gr-icons:help-outline"
- title="read documentation"
- ></iron-icon>
- </a>
- </gr-autocomplete>
- </form>
-`;
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 b6d0579..a97ae96 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
@@ -17,9 +17,9 @@
import '../../../test/common-test-setup-karma';
import './gr-search-bar';
-import '../../../scripts/util';
import {GrSearchBar} from './gr-search-bar';
-import {stubRestApi, mockPromise} from '../../../test/test-utils';
+import '../../../scripts/util';
+import {mockPromise} from '../../../test/test-utils';
import {_testOnly_clearDocsBaseUrlCache} from '../../../utils/url-util';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import {
@@ -28,6 +28,9 @@
createServerInfo,
} from '../../../test/test-data-generators';
import {MergeabilityComputationBehavior} from '../../../constants/constants';
+import {queryAndAssert} from '../../../test/test-utils';
+import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {PaperInputElement} from '@polymer/paper-input/paper-input';
const basicFixture = fixtureFromElement('gr-search-bar');
@@ -36,12 +39,13 @@
setup(async () => {
element = basicFixture.instantiate();
- await flush();
+ await element.updateComplete;
});
- test('value is propagated to _inputVal', () => {
+ test('value is propagated to inputVal', async () => {
element.value = 'foo';
- assert.equal(element._inputVal, 'foo');
+ await element.updateComplete;
+ assert.equal(element.inputVal, 'foo');
});
const getActiveElement = () =>
@@ -52,12 +56,17 @@
test('enter in search input fires event', async () => {
const promise = mockPromise();
element.addEventListener('handle-search', () => {
- assert.notEqual(getActiveElement(), element.$.searchInput);
+ assert.notEqual(
+ getActiveElement(),
+ queryAndAssert<GrAutocomplete>(element, '#searchInput')
+ );
promise.resolve();
});
element.value = 'test';
+ await element.updateComplete;
+ const searchInput = queryAndAssert<GrAutocomplete>(element, '#searchInput');
MockInteractions.pressAndReleaseKeyOn(
- element.$.searchInput.$.input,
+ queryAndAssert<HTMLInputElement>(searchInput, '#input'),
13,
null,
'enter'
@@ -65,11 +74,21 @@
await promise;
});
- test('input blurred after commit', () => {
- const blurSpy = sinon.spy(element.$.searchInput.$.input, 'blur');
- element.$.searchInput.text = 'fate/stay';
+ test('input blurred after commit', async () => {
+ const blurSpy = sinon.spy(
+ queryAndAssert<PaperInputElement>(
+ queryAndAssert<GrAutocomplete>(element, '#searchInput'),
+ '#input'
+ ),
+ 'blur'
+ );
+ queryAndAssert<GrAutocomplete>(element, '#searchInput').text = 'fate/stay';
+ await element.updateComplete;
MockInteractions.pressAndReleaseKeyOn(
- element.$.searchInput.$.input,
+ queryAndAssert<PaperInputElement>(
+ queryAndAssert<GrAutocomplete>(element, '#searchInput'),
+ '#input'
+ ),
13,
null,
'enter'
@@ -77,12 +96,14 @@
assert.isTrue(blurSpy.called);
});
- test('empty search query does not trigger nav', () => {
+ test('empty search query does not trigger nav', async () => {
const searchSpy = sinon.spy();
element.addEventListener('handle-search', searchSpy);
element.value = '';
+ await element.updateComplete;
+ const searchInput = queryAndAssert<GrAutocomplete>(element, '#searchInput');
MockInteractions.pressAndReleaseKeyOn(
- element.$.searchInput.$.input,
+ queryAndAssert<HTMLInputElement>(searchInput, '#input'),
13,
null,
'enter'
@@ -90,12 +111,14 @@
assert.isFalse(searchSpy.called);
});
- test('Predefined query op with no predication doesnt trigger nav', () => {
+ test('Predefined query op with no predication doesnt trigger nav', async () => {
const searchSpy = sinon.spy();
element.addEventListener('handle-search', searchSpy);
element.value = 'added:';
+ await element.updateComplete;
+ const searchInput = queryAndAssert<GrAutocomplete>(element, '#searchInput');
MockInteractions.pressAndReleaseKeyOn(
- element.$.searchInput.$.input,
+ queryAndAssert<HTMLInputElement>(searchInput, '#input'),
13,
null,
'enter'
@@ -103,12 +126,14 @@
assert.isFalse(searchSpy.called);
});
- test('predefined predicate query triggers nav', () => {
+ test('predefined predicate query triggers nav', async () => {
const searchSpy = sinon.spy();
element.addEventListener('handle-search', searchSpy);
element.value = 'age:1week';
+ await element.updateComplete;
+ const searchInput = queryAndAssert<GrAutocomplete>(element, '#searchInput');
MockInteractions.pressAndReleaseKeyOn(
- element.$.searchInput.$.input,
+ queryAndAssert<HTMLInputElement>(searchInput, '#input'),
13,
null,
'enter'
@@ -116,12 +141,14 @@
assert.isTrue(searchSpy.called);
});
- test('undefined predicate query triggers nav', () => {
+ test('undefined predicate query triggers nav', async () => {
const searchSpy = sinon.spy();
element.addEventListener('handle-search', searchSpy);
element.value = 'random:1week';
+ await element.updateComplete;
+ const searchInput = queryAndAssert<GrAutocomplete>(element, '#searchInput');
MockInteractions.pressAndReleaseKeyOn(
- element.$.searchInput.$.input,
+ queryAndAssert<HTMLInputElement>(searchInput, '#input'),
13,
null,
'enter'
@@ -129,12 +156,14 @@
assert.isTrue(searchSpy.called);
});
- test('empty undefined predicate query triggers nav', () => {
+ test('empty undefined predicate query triggers nav', async () => {
const searchSpy = sinon.spy();
element.addEventListener('handle-search', searchSpy);
element.value = 'random:';
+ await element.updateComplete;
+ const searchInput = queryAndAssert<GrAutocomplete>(element, '#searchInput');
MockInteractions.pressAndReleaseKeyOn(
- element.$.searchInput.$.input,
+ queryAndAssert<HTMLInputElement>(searchInput, '#input'),
13,
null,
'enter'
@@ -142,58 +171,67 @@
assert.isTrue(searchSpy.called);
});
- test('keyboard shortcuts', () => {
- const focusSpy = sinon.spy(element.$.searchInput, 'focus');
- const selectAllSpy = sinon.spy(element.$.searchInput, 'selectAll');
+ test('keyboard shortcuts', async () => {
+ const focusSpy = sinon.spy(
+ queryAndAssert<GrAutocomplete>(element, '#searchInput'),
+ 'focus'
+ );
+ const selectAllSpy = sinon.spy(
+ queryAndAssert<GrAutocomplete>(element, '#searchInput'),
+ 'selectAll'
+ );
MockInteractions.pressAndReleaseKeyOn(document.body, 191, null, '/');
assert.isTrue(focusSpy.called);
assert.isTrue(selectAllSpy.called);
});
- suite('_getSearchSuggestions', () => {
- setup(() => {
- // Ensure that config.change.mergeability_computation_behavior is not set.
+ suite('getSearchSuggestions', () => {
+ setup(async () => {
element = basicFixture.instantiate();
+ element.serverConfig = {
+ ...createServerInfo(),
+ change: {
+ ...createChangeConfig(),
+ mergeability_computation_behavior:
+ 'NEVER' as MergeabilityComputationBehavior,
+ },
+ };
+ await element.updateComplete;
});
- test('Autocompletes accounts', () => {
- sinon
- .stub(element, 'accountSuggestions')
- .callsFake(() => Promise.resolve([{text: 'owner:fred@goog.co'}]));
- return element._getSearchSuggestions('owner:fr').then(s => {
- assert.equal(s[0].value, 'owner:fred@goog.co');
- });
+ test('Autocompletes accounts', async () => {
+ element.accountSuggestions = () =>
+ Promise.resolve([{text: 'owner:fred@goog.co'}]);
+ await element.updateComplete;
+ const s = await element.getSearchSuggestions('owner:fr');
+ assert.equal(s[0].value, 'owner:fred@goog.co');
});
test('Autocompletes groups', async () => {
- sinon
- .stub(element, 'groupSuggestions')
- .callsFake(() =>
- Promise.resolve([
- {text: 'ownerin:Polygerrit'},
- {text: 'ownerin:gerrit'},
- ])
- );
- const s = await element._getSearchSuggestions('ownerin:pol');
+ element.groupSuggestions = () =>
+ Promise.resolve([
+ {text: 'ownerin:Polygerrit'},
+ {text: 'ownerin:gerrit'},
+ ]);
+ await element.updateComplete;
+ const s = await element.getSearchSuggestions('ownerin:pol');
assert.equal(s[0].value, 'ownerin:Polygerrit');
});
test('Autocompletes projects', async () => {
- sinon
- .stub(element, 'projectSuggestions')
- .callsFake(() =>
- Promise.resolve([
- {text: 'project:Polygerrit'},
- {text: 'project:gerrit'},
- {text: 'project:gerrittest'},
- ])
- );
- const s = await element._getSearchSuggestions('project:pol');
+ element.projectSuggestions = () =>
+ Promise.resolve([
+ {text: 'project:Polygerrit'},
+ {text: 'project:gerrit'},
+ {text: 'project:gerrittest'},
+ ]);
+ await element.updateComplete;
+ const s = await element.getSearchSuggestions('project:pol');
assert.equal(s[0].value, 'project:Polygerrit');
});
test('Autocompletes simple searches', async () => {
- const s = await element._getSearchSuggestions('is:o');
+ const s = await element.getSearchSuggestions('is:o');
assert.equal(s[0].name, 'is:open');
assert.equal(s[0].value, 'is:open');
assert.equal(s[1].name, 'is:owner');
@@ -201,12 +239,12 @@
});
test('Does not autocomplete with no match', async () => {
- const s = await element._getSearchSuggestions('asdasdasdasd');
+ const s = await element.getSearchSuggestions('asdasdasdasd');
assert.equal(s.length, 0);
});
test('Autocompletes without is:mergable when disabled', async () => {
- const s = await element._getSearchSuggestions('is:mergeab');
+ const s = await element.getSearchSuggestions('is:mergeab');
assert.isEmpty(s);
});
});
@@ -217,23 +255,20 @@
].forEach(mergeability => {
suite(`mergeability as ${mergeability}`, () => {
setup(async () => {
- stubRestApi('getConfig').returns(
- Promise.resolve({
- ...createServerInfo(),
- change: {
- ...createChangeConfig(),
- mergeability_computation_behavior:
- mergeability as MergeabilityComputationBehavior,
- },
- })
- );
-
element = basicFixture.instantiate();
- await flush();
+ element.serverConfig = {
+ ...createServerInfo(),
+ change: {
+ ...createChangeConfig(),
+ mergeability_computation_behavior:
+ mergeability as MergeabilityComputationBehavior,
+ },
+ };
+ await element.updateComplete;
});
test('Autocompltes with is:mergable when enabled', async () => {
- const s = await element._getSearchSuggestions('is:mergeab');
+ const s = await element.getSearchSuggestions('is:mergeab');
assert.equal(s.length, 2);
assert.equal(s[0].name, 'is:mergeable');
assert.equal(s[0].value, 'is:mergeable');
@@ -245,32 +280,30 @@
suite('doc url', () => {
setup(async () => {
- stubRestApi('getConfig').returns(
- Promise.resolve({
- ...createServerInfo(),
- gerrit: {
- ...createGerritInfo(),
- doc_url: 'https://doc.com/',
- },
- })
- );
-
_testOnly_clearDocsBaseUrlCache();
element = basicFixture.instantiate();
- await flush();
+ element.serverConfig = {
+ ...createServerInfo(),
+ gerrit: {
+ ...createGerritInfo(),
+ doc_url: 'https://doc.com/',
+ },
+ };
+ await element.updateComplete;
});
test('compute help doc url with correct path', () => {
assert.equal(element.docBaseUrl, 'https://doc.com/');
assert.equal(
- element._computeHelpDocLink(element.docBaseUrl),
+ element.computeHelpDocLink(),
'https://doc.com/user-search.html'
);
});
test('compute help doc url fallback to gerrit url', () => {
+ element.docBaseUrl = null;
assert.equal(
- element._computeHelpDocLink(null),
+ element.computeHelpDocLink(),
'https://gerrit-review.googlesource.com/documentation/' +
'user-search.html'
);
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 f5a28f8..ed6b822 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
@@ -15,18 +15,17 @@
* limitations under the License.
*/
import '../gr-search-bar/gr-search-bar';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-smart-search_html';
import {GerritNav} from '../gr-navigation/gr-navigation';
import {getUserName} from '../../../utils/display-name-util';
-import {customElement, property} from '@polymer/decorators';
import {AccountInfo, ServerInfo} from '../../../types/common';
import {
SearchBarHandleSearchDetail,
SuggestionProvider,
} from '../gr-search-bar/gr-search-bar';
import {AutocompleteSuggestion} from '../../shared/gr-autocomplete/gr-autocomplete';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
+import {LitElement, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
const MAX_AUTOCOMPLETE_RESULTS = 10;
const SELF_EXPRESSION = 'self';
@@ -36,49 +35,45 @@
interface HTMLElementEventMap {
'handle-search': CustomEvent<SearchBarHandleSearchDetail>;
}
+ interface HTMLElementTagNameMap {
+ 'gr-smart-search': GrSmartSearch;
+ }
}
@customElement('gr-smart-search')
-export class GrSmartSearch extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrSmartSearch extends LitElement {
@property({type: String})
searchQuery = '';
@property({type: Object})
- _config?: ServerInfo;
-
- @property({type: Object})
- _projectSuggestions: SuggestionProvider = (predicate, expression) =>
- this._fetchProjects(predicate, expression);
-
- @property({type: Object})
- _groupSuggestions: SuggestionProvider = (predicate, expression) =>
- this._fetchGroups(predicate, expression);
-
- @property({type: Object})
- _accountSuggestions: SuggestionProvider = (predicate, expression) =>
- this._fetchAccounts(predicate, expression);
+ serverConfig?: ServerInfo;
@property({type: String})
label = '';
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
- override connectedCallback() {
- super.connectedCallback();
- this.restApiService.getConfig().then(cfg => {
- this._config = cfg;
- });
- }
-
- _handleSearch(e: CustomEvent<SearchBarHandleSearchDetail>) {
- const input = e.detail.inputVal;
- if (input) {
- GerritNav.navigateToSearchQuery(input);
- }
+ override render() {
+ const accountSuggestions: SuggestionProvider = (predicate, expression) =>
+ this.fetchAccounts(predicate, expression);
+ const groupSuggestions: SuggestionProvider = (predicate, expression) =>
+ this.fetchGroups(predicate, expression);
+ const projectSuggestions: SuggestionProvider = (predicate, expression) =>
+ this.fetchProjects(predicate, expression);
+ return html`
+ <gr-search-bar
+ id="search"
+ .label=${this.label}
+ .value=${this.searchQuery}
+ .projectSuggestions=${projectSuggestions}
+ .groupSuggestions=${groupSuggestions}
+ .accountSuggestions=${accountSuggestions}
+ .serverConfig=${this.serverConfig}
+ @handle-search=${(e: CustomEvent<SearchBarHandleSearchDetail>) => {
+ this.handleSearch(e);
+ }}
+ ></gr-search-bar>
+ `;
}
/**
@@ -88,8 +83,10 @@
* 'project'
* @param expression - The second part of the search term, e.g.
* 'gerr'
+ *
+ * private but used in test
*/
- _fetchProjects(
+ fetchProjects(
predicate: string,
expression: string
): Promise<AutocompleteSuggestion[]> {
@@ -113,8 +110,10 @@
* 'ownerin'
* @param expression - The second part of the search term, e.g.
* 'polyger'
+ *
+ * private but used in test
*/
- _fetchGroups(
+ fetchGroups(
predicate: string,
expression: string
): Promise<AutocompleteSuggestion[]> {
@@ -141,8 +140,10 @@
* 'owner'
* @param expression - The second part of the search term, e.g.
* 'kasp'
+ *
+ * private but used in test
*/
- _fetchAccounts(
+ fetchAccounts(
predicate: string,
expression: string
): Promise<AutocompleteSuggestion[]> {
@@ -155,7 +156,7 @@
if (!accounts) {
return [];
}
- return this._mapAccountsHelper(accounts, predicate);
+ return this.mapAccountsHelper(accounts, predicate);
})
.then(accounts => {
// When the expression supplied is a beginning substring of 'self',
@@ -170,12 +171,12 @@
});
}
- _mapAccountsHelper(
+ private mapAccountsHelper(
accounts: AccountInfo[],
predicate: string
): AutocompleteSuggestion[] {
return accounts.map(account => {
- const userName = getUserName(this._config, account);
+ const userName = getUserName(this.serverConfig, account);
return {
label: account.name || '',
text: account.email
@@ -184,10 +185,11 @@
};
});
}
-}
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-smart-search': GrSmartSearch;
+ private handleSearch(e: CustomEvent<SearchBarHandleSearchDetail>) {
+ const input = e.detail.inputVal;
+ if (input) {
+ GerritNav.navigateToSearchQuery(input);
+ }
}
}
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.ts b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.ts
deleted file mode 100644
index c08df15..0000000
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles"></style>
- <gr-search-bar
- id="search"
- label="[[label]]"
- value="{{searchQuery}}"
- on-handle-search="_handleSearch"
- project-suggestions="[[_projectSuggestions]]"
- group-suggestions="[[_groupSuggestions]]"
- account-suggestions="[[_accountSuggestions]]"
- ></gr-search-bar>
-`;
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 0218a8f..779844e 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
@@ -26,8 +26,9 @@
suite('gr-smart-search tests', () => {
let element: GrSmartSearch;
- setup(() => {
+ setup(async () => {
element = basicFixture.instantiate();
+ await element.updateComplete;
});
test('Autocompletes accounts', () => {
@@ -39,7 +40,7 @@
},
])
);
- return element._fetchAccounts('owner', 'fr').then(s => {
+ return element.fetchAccounts('owner', 'fr').then(s => {
assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
});
});
@@ -54,12 +55,12 @@
])
);
element
- ._fetchAccounts('owner', 's')
+ .fetchAccounts('owner', 's')
.then(s => {
assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
assert.deepEqual(s[1], {text: 'owner:self'});
})
- .then(() => element._fetchAccounts('owner', 'selfs'))
+ .then(() => element.fetchAccounts('owner', 'selfs'))
.then(s => {
assert.notEqual(s[0], {text: 'owner:self'});
});
@@ -75,12 +76,12 @@
])
);
return element
- ._fetchAccounts('owner', 'm')
+ .fetchAccounts('owner', 'm')
.then(s => {
assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'});
assert.deepEqual(s[1], {text: 'owner:me'});
})
- .then(() => element._fetchAccounts('owner', 'meme'))
+ .then(() => element.fetchAccounts('owner', 'meme'))
.then(s => {
assert.notEqual(s[0], {text: 'owner:me'});
});
@@ -94,7 +95,7 @@
gerrittest: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f' as GroupId},
})
);
- return element._fetchGroups('ownerin', 'pol').then(s => {
+ return element.fetchGroups('ownerin', 'pol').then(s => {
assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'});
});
});
@@ -103,7 +104,7 @@
stubRestApi('getSuggestedProjects').callsFake(() =>
Promise.resolve({Polygerrit: {id: 'test' as UrlEncodedRepoName}})
);
- return element._fetchProjects('project', 'pol').then(s => {
+ return element.fetchProjects('project', 'pol').then(s => {
assert.deepEqual(s[0], {text: 'project:Polygerrit'});
});
});
@@ -116,7 +117,7 @@
gerrittest: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f' as GroupId},
})
);
- return element._fetchGroups('ownerin', 'gerrit').then(s => {
+ return element.fetchGroups('ownerin', 'gerrit').then(s => {
assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'});
assert.deepEqual(s[1], {text: 'ownerin:gerrit'});
assert.deepEqual(s[2], {text: 'ownerin:gerrittest'});
@@ -127,7 +128,7 @@
stubRestApi('getSuggestedAccounts').callsFake(() =>
Promise.resolve([{name: 'fred'}])
);
- return element._fetchAccounts('owner', 'fr').then(s => {
+ return element.fetchAccounts('owner', 'fr').then(s => {
assert.deepEqual(s[0], {text: 'owner:"fred"', label: 'fred'});
});
});
@@ -136,7 +137,7 @@
stubRestApi('getSuggestedAccounts').callsFake(() =>
Promise.resolve([{email: 'fred@goog.co' as EmailAddress}])
);
- return element._fetchAccounts('owner', 'fr').then(s => {
+ return element.fetchAccounts('owner', 'fr').then(s => {
assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: ''});
});
});
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 a6176e1..fb05b02 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
@@ -36,7 +36,7 @@
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {isRobot} from '../../../utils/comment-util';
import {OpenFixPreviewEvent} from '../../../types/events';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {fireCloseFixPreview, fireEvent} from '../../../utils/event-util';
import {DiffLayer, ParsedChangeInfo} from '../../../types/types';
import {GrButton} from '../../shared/gr-button/gr-button';
@@ -103,7 +103,7 @@
private refitOverlay?: () => void;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
@@ -293,11 +293,10 @@
.applyFixSuggestion(changeNum, patchNum, this._currentFix.fix_id)
.then(res => {
if (res && res.ok) {
- GerritNav.navigateToChange(
- change,
- EditPatchSetNum,
- patchNum as BasePatchSetNum
- );
+ GerritNav.navigateToChange(change, {
+ patchNum: EditPatchSetNum,
+ basePatchNum: patchNum as BasePatchSetNum,
+ });
this._close(true);
}
this._isApplyFixLoading = false;
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 94d37f5..9392cb9d1 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
@@ -24,6 +24,7 @@
BasePatchSetNum,
EditPatchSetNum,
PatchSetNum,
+ RobotCommentInfo,
RobotId,
RobotRunId,
Timestamp,
@@ -37,7 +38,6 @@
} from '../../../test/test-data-generators';
import {createDefaultDiffPrefs} from '../../../constants/constants';
import {DiffInfo} from '../../../types/diff';
-import {UIRobot} from '../../../utils/comment-util';
import {
CloseFixPreviewEventDetail,
EventType,
@@ -50,7 +50,7 @@
suite('gr-apply-fix-dialog tests', () => {
let element: GrApplyFixDialog;
- const ROBOT_COMMENT_WITH_TWO_FIXES: UIRobot = {
+ const ROBOT_COMMENT_WITH_TWO_FIXES: RobotCommentInfo = {
id: '1' as UrlEncodedCommentId,
updated: '2018-02-08 18:49:18.000000000' as Timestamp,
robot_id: 'robot_1' as RobotId,
@@ -62,7 +62,7 @@
],
};
- const ROBOT_COMMENT_WITH_ONE_FIX: UIRobot = {
+ const ROBOT_COMMENT_WITH_ONE_FIX: RobotCommentInfo = {
id: '2' as UrlEncodedCommentId,
updated: '2018-02-08 18:49:18.000000000' as Timestamp,
robot_id: 'robot_1' as RobotId,
@@ -277,12 +277,10 @@
2 as PatchSetNum,
'123'
);
- sinon.assert.calledWithExactly(
- navigateToChangeStub,
- element.change!,
- EditPatchSetNum,
- element.change!.revisions[2]._number as BasePatchSetNum
- );
+ sinon.assert.calledWithExactly(navigateToChangeStub, element.change!, {
+ patchNum: EditPatchSetNum,
+ basePatchNum: element.change!.revisions[2]._number as BasePatchSetNum,
+ });
sinon.assert.calledOnceWithExactly(
closeFixPreviewEventSpy,
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 fc22a58..a08bf39 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
@@ -14,16 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-comment-api_html';
-import {customElement, property} from '@polymer/decorators';
import {
- CommentBasics,
PatchRange,
PatchSetNum,
RobotCommentInfo,
UrlEncodedCommentId,
- NumericChangeId,
PathToCommentsInfoMap,
FileInfo,
ParentPatchSetNum,
@@ -35,18 +30,14 @@
CommentThread,
DraftInfo,
isUnresolved,
- UIComment,
createCommentThreads,
isInPatchRange,
isDraftThread,
- isInBaseOfPatchRange,
- isInRevisionOfPatchRange,
isPatchsetLevel,
addPath,
} from '../../../utils/comment-util';
import {PatchSetFile, PatchNumOnly, isPatchSetFile} from '../../../types/types';
-import {appContext} from '../../../services/app-context';
-import {CommentSide, Side} from '../../../constants/constants';
+import {CommentSide} from '../../../constants/constants';
import {pluralize} from '../../../utils/string-util';
import {NormalizedFileInfo} from '../../change/gr-file-list/gr-file-list';
@@ -70,11 +61,11 @@
* elements of that which uses the gr-comment-api.
*/
constructor(
- comments: PathToCommentsInfoMap | undefined,
- robotComments: {[path: string]: RobotCommentInfo[]} | undefined,
- drafts: {[path: string]: DraftInfo[]} | undefined,
- portedComments: PathToCommentsInfoMap | undefined,
- portedDrafts: PathToCommentsInfoMap | undefined
+ comments?: PathToCommentsInfoMap,
+ robotComments?: {[path: string]: RobotCommentInfo[]},
+ drafts?: {[path: string]: DraftInfo[]},
+ portedComments?: PathToCommentsInfoMap,
+ portedDrafts?: PathToCommentsInfoMap
) {
this._comments = addPath(comments);
this._robotComments = addPath(robotComments);
@@ -119,7 +110,7 @@
* patchNum and basePatchNum properties to represent the range.
*/
getPaths(patchRange?: PatchRange): CommentMap {
- const responses: {[path: string]: UIComment[]}[] = [
+ const responses: {[path: string]: Comment[]}[] = [
this._comments,
this.drafts,
this._robotComments,
@@ -144,25 +135,11 @@
}
/**
- * Gets all the comments for a particular thread group. Used for refreshing
- * comments after the thread group has already been built.
- */
- getCommentsForThread(rootId: UrlEncodedCommentId) {
- const allThreads = this.getAllThreadsForChange();
- const threadMatch = allThreads.find(t => t.rootId === rootId);
-
- // In the event that a single draft comment was removed by the thread-list
- // and the diff view is updating comments, there will no longer be a thread
- // found. In this case, return null.
- return threadMatch ? threadMatch.comments : null;
- }
-
- /**
* Gets all the comments and robot comments for the given change.
*/
getAllComments(includeDrafts?: boolean, patchNum?: PatchSetNum) {
const paths = this.getPaths();
- const publishedComments: {[path: string]: CommentBasics[]} = {};
+ const publishedComments: {[path: string]: CommentInfo[]} = {};
for (const path of Object.keys(paths)) {
publishedComments[path] = this.getAllCommentsForPath(
path,
@@ -196,8 +173,8 @@
path: string,
patchNum?: PatchSetNum,
includeDrafts?: boolean
- ): Comment[] {
- const comments: Comment[] = this._comments[path] || [];
+ ): CommentInfo[] {
+ const comments: CommentInfo[] = this._comments[path] || [];
const robotComments = this._robotComments[path] || [];
let allComments = comments.concat(robotComments);
if (includeDrafts) {
@@ -233,43 +210,18 @@
return allComments;
}
- cloneWithUpdatedDrafts(drafts: {[path: string]: DraftInfo[]} | undefined) {
- return new ChangeComments(
- this._comments,
- this._robotComments,
- drafts,
- this._portedComments,
- this._portedDrafts
- );
- }
-
- cloneWithUpdatedPortedComments(
- portedComments?: PathToCommentsInfoMap,
- portedDrafts?: PathToCommentsInfoMap
- ) {
- return new ChangeComments(
- this._comments,
- this._robotComments,
- this._drafts,
- portedComments,
- portedDrafts
- );
- }
-
/**
* Get the drafts for a path and optional patch num.
*
* This will return a shallow copy of all drafts every time,
* so changes on any copy will not affect other copies.
*/
- getAllDraftsForPath(path: string, patchNum?: PatchSetNum): Comment[] {
- let comments = this._drafts[path] || [];
+ getAllDraftsForPath(path: string, patchNum?: PatchSetNum): DraftInfo[] {
+ let drafts = this._drafts[path] || [];
if (patchNum) {
- comments = comments.filter(c => c.patch_set === patchNum);
+ drafts = drafts.filter(c => c.patch_set === patchNum);
}
- return comments.map(c => {
- return {...c, __draft: true};
- });
+ return drafts;
}
/**
@@ -277,7 +229,7 @@
*
* // TODO(taoalpha): maybe merge in *ForPath
*/
- getAllDraftsForFile(file: PatchSetFile): Comment[] {
+ getAllDraftsForFile(file: PatchSetFile): CommentInfo[] {
let allDrafts = this.getAllDraftsForPath(file.path, file.patchNum);
if (file.basePath) {
allDrafts = allDrafts.concat(
@@ -297,8 +249,8 @@
* @param projectConfig Optional project config object to
* include in the meta sub-object.
*/
- getCommentsForPath(path: string, patchRange: PatchRange): Comment[] {
- let comments: Comment[] = [];
+ getCommentsForPath(path: string, patchRange: PatchRange): CommentInfo[] {
+ let comments: CommentInfo[] = [];
let drafts: DraftInfo[] = [];
let robotComments: RobotCommentInfo[] = [];
if (this._comments && this._comments[path]) {
@@ -311,17 +263,13 @@
robotComments = this._robotComments[path];
}
- drafts.forEach(d => {
- d.__draft = true;
- });
-
- return comments
- .concat(drafts)
- .concat(robotComments)
+ const all = comments.concat(drafts).concat(robotComments);
+ const final = all
.filter(c => isInPatchRange(c, patchRange))
.map(c => {
return {...c};
});
+ return final;
}
/**
@@ -372,7 +320,7 @@
// ported comments will involve comments that may not belong to the
// current patchrange, so we need to form threads for them using all
// comments
- const allComments: UIComment[] = this.getAllCommentsForFile(file, true);
+ const allComments: CommentInfo[] = this.getAllCommentsForFile(file, true);
return createCommentThreads(allComments).filter(thread => {
// Robot comments and drafts are not ported over. A human reply to
@@ -388,22 +336,9 @@
comment => comment.id === portedComment.id
)!;
- if (
- (originalComment.line && !portedComment.line) ||
- (originalComment.range && !portedComment.range)
- ) {
- thread.rangeInfoLost = true;
- }
+ // Original comment shown anyway? No need to port.
+ if (isInPatchRange(originalComment, patchRange)) return false;
- if (
- isInBaseOfPatchRange(thread.comments[0], patchRange) ||
- isInRevisionOfPatchRange(thread.comments[0], patchRange)
- ) {
- // no need to port this thread as it will be rendered by default
- return false;
- }
-
- thread.diffSide = Side.RIGHT;
if (thread.commentSide === CommentSide.PARENT) {
// TODO(dhruvsri): Add handling for merge parents
if (
@@ -411,11 +346,21 @@
!!thread.mergeParentNum
)
return false;
- thread.diffSide = Side.LEFT;
}
if (!isUnresolved(thread) && !isDraftThread(thread)) return false;
+ if (
+ (originalComment.line && !portedComment.line) ||
+ (originalComment.range && !portedComment.range)
+ ) {
+ thread.rangeInfoLost = true;
+ }
+ // TODO: It probably makes more sense to set the patch_set in
+ // portedComment either in the backend or in the RestApi layer. Then we
+ // could check `!isInPatchRange(portedComment, patchRange)` and then set
+ // thread.patchNum = portedComment.patch_set;
+ thread.patchNum = patchRange.patchNum;
thread.range = portedComment.range;
thread.line = portedComment.line;
thread.ported = true;
@@ -428,8 +373,7 @@
patchRange: PatchRange
): CommentThread[] {
const threads = createCommentThreads(
- this.getCommentsForFile(file, patchRange),
- patchRange
+ this.getCommentsForFile(file, patchRange)
);
threads.push(...this._getPortedCommentThreads(file, patchRange));
return threads;
@@ -447,7 +391,10 @@
* @param projectConfig Optional project config object to
* include in the meta sub-object.
*/
- getCommentsForFile(file: PatchSetFile, patchRange: PatchRange): Comment[] {
+ getCommentsForFile(
+ file: PatchSetFile,
+ patchRange: PatchRange
+ ): CommentInfo[] {
const comments = this.getCommentsForPath(file.path, patchRange);
if (file.basePath) {
comments.push(...this.getCommentsForPath(file.basePath, patchRange));
@@ -469,11 +416,11 @@
file: PatchSetFile | PatchNumOnly,
ignorePatchsetLevelComments?: boolean
) {
- let comments: Comment[] = [];
+ let comments: CommentInfo[] = [];
if (isPatchSetFile(file)) {
comments = this.getAllCommentsForFile(file);
} else {
- comments = this._commentObjToArray(
+ comments = this._commentObjToArray<CommentInfo>(
this.getAllPublishedComments(file.patchNum)
);
}
@@ -584,8 +531,8 @@
file: PatchSetFile | PatchNumOnly,
ignorePatchsetLevelComments?: boolean
) {
- let comments: Comment[] = [];
- let drafts: Comment[] = [];
+ let comments: CommentInfo[] = [];
+ let drafts: CommentInfo[] = [];
if (isPatchSetFile(file)) {
comments = this.getAllCommentsForFile(file);
@@ -611,38 +558,3 @@
return createCommentThreads(comments);
}
}
-
-@customElement('gr-comment-api')
-export class GrCommentApi extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
- @property({type: Object})
- _changeComments?: ChangeComments;
-
- private readonly restApiService = appContext.restApiService;
-
- private readonly commentsService = appContext.commentsService;
-
- reloadPortedComments(changeNum: NumericChangeId, patchNum: PatchSetNum) {
- if (!this._changeComments) {
- this.commentsService.loadAll(changeNum);
- return Promise.resolve();
- }
- return Promise.all([
- this.restApiService.getPortedComments(changeNum, patchNum),
- this.restApiService.getPortedDrafts(changeNum, patchNum),
- ]).then(res => {
- if (!this._changeComments) return;
- this._changeComments =
- this._changeComments.cloneWithUpdatedPortedComments(res[0], res[1]);
- });
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-comment-api': GrCommentApi;
- }
-}
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js
index 7e01371..2738eb8 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js
@@ -20,7 +20,7 @@
import {ChangeComments} from './gr-comment-api.js';
import {isInRevisionOfPatchRange, isInBaseOfPatchRange, isDraftThread, isUnresolved, createCommentThreads} from '../../../utils/comment-util.js';
import {createDraft, createComment, createChangeComments, createCommentThread} from '../../../test/test-data-generators.js';
-import {CommentSide, Side} from '../../../constants/constants.js';
+import {CommentSide} from '../../../constants/constants.js';
import {stubRestApi} from '../../../test/test-utils.js';
const basicFixture = fixtureFromElement('gr-comment-api');
@@ -131,7 +131,8 @@
{path: 'karma.conf.js'}, {patchNum: 4, basePatchNum: 'PARENT'});
assert.equal(portedThreads.length, 1);
- // check range of thread is from the ported comment and not the original
+ // check that the location of the thread matches the ported comment
+ assert.equal(portedThreads[0].patchNum, 4);
assert.deepEqual(portedThreads[0].range, {
start_line: 136,
start_character: 16,
@@ -207,7 +208,6 @@
{path: 'karma.conf.js'}, {patchNum: 4, basePatchNum: 'PARENT'});
assert.equal(portedThreads.length, 1);
assert.equal(portedThreads[0].line, 31);
- assert.equal(portedThreads[0].diffSide, Side.LEFT);
assert.equal(changeComments._getPortedCommentThreads(
{path: 'karma.conf.js'}, {patchNum: 4, basePatchNum: -2}
@@ -363,6 +363,7 @@
...createComment(),
id: '01',
patch_set: 2,
+ path: 'file/one',
side: PARENT,
line: 1,
updated: makeTime(1),
@@ -379,6 +380,7 @@
id: '02',
in_reply_to: '04',
patch_set: 2,
+ path: 'file/one',
unresolved: true,
line: 1,
updated: makeTime(3),
@@ -388,6 +390,7 @@
...createComment(),
id: '03',
patch_set: 2,
+ path: 'file/one',
side: PARENT,
line: 2,
updated: makeTime(1),
@@ -397,6 +400,7 @@
...createComment(),
id: '04',
patch_set: 2,
+ path: 'file/one',
line: 1,
updated: makeTime(1),
};
@@ -470,6 +474,7 @@
side: PARENT,
line: 1,
updated: makeTime(3),
+ path: 'file/one',
};
commentObjs['13'] = {
@@ -481,6 +486,7 @@
// Draft gets lower timestamp than published comment, because we
// want to test that the draft still gets sorted to the end.
updated: makeTime(2),
+ path: 'file/one',
};
commentObjs['14'] = {
@@ -597,10 +603,6 @@
const path = 'file/one';
const drafts = element._changeComments.getAllDraftsForPath(path);
assert.equal(drafts.length, 2);
- const aCopyOfDrafts = element._changeComments
- .getAllDraftsForPath(path);
- assert.deepEqual(drafts, aCopyOfDrafts);
- assert.notEqual(drafts[0], aCopyOfDrafts[0]);
});
test('computeUnresolvedNum', () => {
@@ -828,24 +830,6 @@
const threads = element._changeComments.getAllThreadsForChange();
assert.deepEqual(threads, expectedThreads);
});
-
- test('getCommentsForThreadGroup', () => {
- let expectedComments = [
- {...commentObjs['04'], path: 'file/one'},
- {...commentObjs['02'], path: 'file/one'},
- {...commentObjs['13'], path: 'file/one'},
- ];
- assert.deepEqual(element._changeComments.getCommentsForThread('04'),
- expectedComments);
-
- expectedComments = [{...commentObjs['12'], path: 'file/one'}];
-
- assert.deepEqual(element._changeComments.getCommentsForThread('12'),
- expectedComments);
-
- assert.deepEqual(element._changeComments.getCommentsForThread('1000'),
- null);
- });
});
});
});
diff --git a/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls.ts b/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls.ts
index af9c9d4..6e43fdc 100644
--- a/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls.ts
+++ b/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls.ts
@@ -24,7 +24,7 @@
import '@polymer/paper-listbox/paper-listbox';
import '@polymer/paper-tooltip/paper-tooltip';
import {of, EMPTY, Subject} from 'rxjs';
-import {switchMap, delay, takeUntil} from 'rxjs/operators';
+import {switchMap, delay} from 'rxjs/operators';
import '../../shared/gr-button/gr-button';
import {pluralize} from '../../../utils/string-util';
@@ -33,6 +33,7 @@
import {assertIsDefined} from '../../../utils/common-util';
import {css, html, LitElement, TemplateResult} from 'lit';
import {customElement, property} from 'lit/decorators';
+import {subscribe} from '../../lit/subscription-controller';
import {
ContextButtonType,
@@ -87,7 +88,7 @@
@property({type: Object}) section?: HTMLElement;
- @property({type: Object}) contextGroups: GrDiffGroup[] = [];
+ @property({type: Object}) group?: GrDiffGroup;
@property({type: String, reflect: true})
showConfig: GrContextControlsShowConfig = 'both';
@@ -98,8 +99,6 @@
linesToExpand: number;
}>();
- private disconnected$ = new Subject();
-
static override styles = css`
:host {
display: flex;
@@ -208,10 +207,6 @@
this.setupButtonHoverHandler();
}
- override disconnectedCallback() {
- this.disconnected$.next();
- }
-
private showBoth() {
return this.showConfig === 'both';
}
@@ -224,9 +219,10 @@
return this.showBoth() || this.showConfig === 'below';
}
- setupButtonHoverHandler() {
- this.expandButtonsHover
- .pipe(
+ private setupButtonHoverHandler() {
+ subscribe(
+ this,
+ this.expandButtonsHover.pipe(
switchMap(e => {
if (e.eventType === 'leave') {
// cancel any previous delay
@@ -234,20 +230,23 @@
return EMPTY;
}
return of(e).pipe(delay(500));
- }),
- takeUntil(this.disconnected$)
- )
- .subscribe(({buttonType, linesToExpand}) => {
+ })
+ ),
+ ({buttonType, linesToExpand}) => {
fire(this, 'diff-context-button-hovered', {
buttonType,
linesToExpand,
});
- });
+ }
+ );
}
private numLines() {
- const {leftStart, leftEnd} = this.contextRange();
- return leftEnd - leftStart + 1;
+ assertIsDefined(this.group);
+ // In context groups, there is the same number of lines left and right
+ const left = this.group.lineRange.left;
+ // Both start and end inclusive, so we need to add 1.
+ return left.end_line - left.start_line + 1;
}
private createExpandAllButtonContainer() {
@@ -266,6 +265,7 @@
linesToExpand: number,
tooltip?: TemplateResult
) {
+ if (!this.group) return;
let text = '';
let groups: GrDiffGroup[] = []; // The groups that replace this one if tapped.
let ariaLabel = '';
@@ -279,14 +279,14 @@
: this.showAbove()
? 'aboveButton'
: 'belowButton';
- if (this.partialContent) {
+ if (this.group?.hasSkipGroup()) {
// Expanding content would require load of more data
text += ' (too large)';
}
- groups.push(...this.contextGroups);
+ groups.push(...this.group.contextGroups);
} else if (type === ContextButtonType.ABOVE) {
groups = hideInContextControl(
- this.contextGroups,
+ this.group.contextGroups,
linesToExpand,
this.numLines()
);
@@ -295,7 +295,7 @@
ariaLabel = `Show ${pluralize(linesToExpand, 'line')} above`;
} else if (type === ContextButtonType.BELOW) {
groups = hideInContextControl(
- this.contextGroups,
+ this.group.contextGroups,
0,
this.numLines() - linesToExpand
);
@@ -304,7 +304,7 @@
ariaLabel = `Show ${pluralize(linesToExpand, 'line')} below`;
} else if (type === ContextButtonType.BLOCK_ABOVE) {
groups = hideInContextControl(
- this.contextGroups,
+ this.group.contextGroups,
linesToExpand,
this.numLines()
);
@@ -313,7 +313,7 @@
ariaLabel = 'Show block above';
} else if (type === ContextButtonType.BLOCK_BELOW) {
groups = hideInContextControl(
- this.contextGroups,
+ this.group.contextGroups,
0,
this.numLines() - linesToExpand
);
@@ -354,21 +354,11 @@
groups: GrDiffGroup[]
) {
return (e: Event) => {
+ assertIsDefined(this.group);
e.stopPropagation();
- if (type === ContextButtonType.ALL && this.partialContent) {
- const {leftStart, leftEnd, rightStart, rightEnd} = this.contextRange();
- const lineRange = {
- left: {
- start_line: leftStart,
- end_line: leftEnd,
- },
- right: {
- start_line: rightStart,
- end_line: rightEnd,
- },
- };
+ if (type === ContextButtonType.ALL && this.group?.hasSkipGroup()) {
fire(this, 'content-load-needed', {
- lineRange,
+ lineRange: this.group.lineRange,
});
} else {
assertIsDefined(this.section, 'section');
@@ -416,20 +406,14 @@
}
/**
- * Checks if the collapsed section contains unavailable content (skip chunks).
- */
- private get partialContent() {
- return this.contextGroups.some(c => !!c.skip);
- }
-
- /**
* Creates a container div with block expansion buttons (above and/or below).
*/
private createBlockExpansionButtons() {
+ assertIsDefined(this.group, 'group');
if (
!this.showPartialLinks() ||
!this.renderPreferences?.use_block_expansion ||
- this.partialContent
+ this.group?.hasSkipGroup()
) {
return undefined;
}
@@ -439,14 +423,14 @@
aboveBlockButton = this.createBlockButton(
ContextButtonType.BLOCK_ABOVE,
this.numLines(),
- this.contextRange().rightStart - 1
+ this.group.lineRange.right.start_line - 1
);
}
if (this.showBelow()) {
belowBlockButton = this.createBlockButton(
ContextButtonType.BLOCK_BELOW,
this.numLines(),
- this.contextRange().rightEnd + 1
+ this.group.lineRange.right.end_line + 1
);
}
if (aboveBlockButton || belowBlockButton) {
@@ -506,21 +490,8 @@
return this.createContextButton(buttonType, linesToExpand, tooltip);
}
- private contextRange() {
- return {
- leftStart: this.contextGroups[0].lineRange.left.start_line,
- leftEnd:
- this.contextGroups[this.contextGroups.length - 1].lineRange.left
- .end_line,
- rightStart: this.contextGroups[0].lineRange.right.start_line,
- rightEnd:
- this.contextGroups[this.contextGroups.length - 1].lineRange.right
- .end_line,
- };
- }
-
private hasValidProperties() {
- return !!(this.diff && this.section && this.contextGroups?.length);
+ return !!(this.diff && this.section && this.group?.contextGroups?.length);
}
override render() {
diff --git a/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls_test.ts b/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls_test.ts
index cacea42..92debda 100644
--- a/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-context-controls/gr-context-controls_test.ts
@@ -39,7 +39,7 @@
await flush();
});
- function createContextGroups(options: {offset?: number; count?: number}) {
+ function createContextGroup(options: {offset?: number; count?: number}) {
const offset = options.offset || 0;
const numLines = options.count || 10;
const lines = [];
@@ -50,12 +50,14 @@
line.text = 'lorem upsum';
lines.push(line);
}
-
- return [new GrDiffGroup(GrDiffGroupType.BOTH, lines)];
+ return new GrDiffGroup({
+ type: GrDiffGroupType.CONTEXT_CONTROL,
+ contextGroups: [new GrDiffGroup({type: GrDiffGroupType.BOTH, lines})],
+ });
}
test('no +10 buttons for 10 or less lines', async () => {
- element.contextGroups = createContextGroups({count: 10});
+ element.group = createContextGroup({count: 10});
await flush();
@@ -67,7 +69,7 @@
});
test('context control at the top', async () => {
- element.contextGroups = createContextGroups({offset: 0, count: 20});
+ element.group = createContextGroup({offset: 0, count: 20});
element.showConfig = 'below';
await flush();
@@ -85,7 +87,7 @@
});
test('context control in the middle', async () => {
- element.contextGroups = createContextGroups({offset: 10, count: 20});
+ element.group = createContextGroup({offset: 10, count: 20});
element.showConfig = 'both';
await flush();
@@ -105,7 +107,7 @@
});
test('context control at the bottom', async () => {
- element.contextGroups = createContextGroups({offset: 30, count: 20});
+ element.group = createContextGroup({offset: 30, count: 20});
element.showConfig = 'above';
await flush();
@@ -131,7 +133,7 @@
test('context control with block expansion at the top', async () => {
prepareForBlockExpansion([]);
- element.contextGroups = createContextGroups({offset: 0, count: 20});
+ element.group = createContextGroup({offset: 0, count: 20});
element.showConfig = 'below';
await flush();
@@ -160,7 +162,7 @@
test('context control with block expansion in the middle', async () => {
prepareForBlockExpansion([]);
- element.contextGroups = createContextGroups({offset: 10, count: 20});
+ element.group = createContextGroup({offset: 10, count: 20});
element.showConfig = 'both';
await flush();
@@ -197,7 +199,7 @@
test('context control with block expansion at the bottom', async () => {
prepareForBlockExpansion([]);
- element.contextGroups = createContextGroups({offset: 30, count: 20});
+ element.group = createContextGroup({offset: 30, count: 20});
element.showConfig = 'above';
await flush();
@@ -237,7 +239,7 @@
children: [],
},
]);
- element.contextGroups = createContextGroups({offset: 10, count: 20});
+ element.group = createContextGroup({offset: 10, count: 20});
element.showConfig = 'both';
await flush();
@@ -289,7 +291,7 @@
],
},
]);
- element.contextGroups = createContextGroups({offset: 10, count: 20});
+ element.group = createContextGroup({offset: 10, count: 20});
element.showConfig = 'both';
await flush();
@@ -335,7 +337,7 @@
],
},
]);
- element.contextGroups = createContextGroups({offset: 10, count: 20});
+ element.group = createContextGroup({offset: 10, count: 20});
element.showConfig = 'both';
await flush();
@@ -353,7 +355,7 @@
test('+Block tooltip shows "all common lines" for empty syntax tree', async () => {
prepareForBlockExpansion([]);
- element.contextGroups = createContextGroups({offset: 10, count: 20});
+ element.group = createContextGroup({offset: 10, count: 20});
element.showConfig = 'both';
await flush();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
index 720cc27..ee44b2b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.ts
@@ -22,7 +22,7 @@
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-diff-builder-element_html';
import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
-import {GrDiffBuilder} from './gr-diff-builder';
+import {DiffContextExpandedEventDetail, GrDiffBuilder} from './gr-diff-builder';
import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side';
import {GrDiffBuilderImage} from './gr-diff-builder-image';
import {GrDiffBuilderUnified} from './gr-diff-builder-unified';
@@ -42,12 +42,17 @@
} from '../gr-ranged-comment-layer/gr-ranged-comment-layer';
import {GrCoverageLayer} from '../gr-coverage-layer/gr-coverage-layer';
import {DiffViewMode, RenderPreferences} from '../../../api/diff';
-import {Side} from '../../../constants/constants';
+import {createDefaultDiffPrefs, Side} from '../../../constants/constants';
import {GrDiffLine, LineNumber} from '../gr-diff/gr-diff-line';
-import {GrDiffGroup} from '../gr-diff/gr-diff-group';
+import {
+ GrDiffGroup,
+ GrDiffGroupType,
+ hideInContextControl,
+} from '../gr-diff/gr-diff-group';
import {PolymerSpliceChange} from '@polymer/polymer/interfaces';
import {getLineNumber, getSideByLineEl} from '../gr-diff/gr-diff-utils';
import {fireAlert, fireEvent} from '../../../utils/event-util';
+import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
const TRAILING_WHITESPACE_PATTERN = /\s+$/;
@@ -139,8 +144,16 @@
path?: string;
@property({type: Object})
+ prefs: DiffPreferencesInfo = createDefaultDiffPrefs();
+
+ @property({type: Object})
+ renderPrefs?: RenderPreferences;
+
+ @property({type: Object})
_builder?: GrDiffBuilder;
+ // This is written to only from the processor via property notify
+ // And then passed to the builder via a property observer.
@property({type: Array})
_groups: GrDiffGroup[] = [];
@@ -191,6 +204,20 @@
@property({type: Object})
_cancelableRenderPromise: CancelablePromise<unknown> | null = null;
+ constructor() {
+ super();
+ afterNextRender(this, () => {
+ this.addEventListener(
+ 'diff-context-expanded',
+ (e: CustomEvent<DiffContextExpandedEventDetail>) => {
+ // Don't stop propagation. The host may listen for reporting or
+ // resizing.
+ this.rerenderSection(e.detail.groups, e.detail.section);
+ }
+ );
+ });
+ }
+
override disconnectedCallback() {
if (this._builder) {
this._builder.clear();
@@ -211,19 +238,15 @@
return coverageRanges.filter(range => range && range.side === 'right');
}
- render(
- keyLocations: KeyLocations,
- prefs: DiffPreferencesInfo,
- renderPrefs?: RenderPreferences
- ) {
+ render(keyLocations: KeyLocations) {
// Setting up annotation layers must happen after plugins are
// installed, and |render| satisfies the requirement, however,
// |attached| doesn't because in the diff view page, the element is
// attached before plugins are installed.
this._setupAnnotationLayers();
- this._showTabs = !!prefs.show_tabs;
- this._showTrailingWhitespace = !!prefs.show_whitespace_errors;
+ this._showTabs = this.prefs.show_tabs;
+ this._showTrailingWhitespace = this.prefs.show_whitespace_errors;
// Stop the processor if it's running.
this.cancel();
@@ -234,13 +257,16 @@
if (!this.diff) {
throw Error('Cannot render a diff without DiffInfo.');
}
- this._builder = this._getDiffBuilder(this.diff, prefs, renderPrefs);
+ this._builder = this._getDiffBuilder();
- this.$.processor.context = prefs.context;
+ this.$.processor.context = this.prefs.context;
this.$.processor.keyLocations = keyLocations;
this._clearDiffContent();
- this._builder.addColumns(this.diffElement, getLineNumberCellWidth(prefs));
+ this._builder.addColumns(
+ this.diffElement,
+ getLineNumberCellWidth(this.prefs)
+ );
const isBinary = !!(this.isImageDiff || this.diff.binary);
@@ -315,24 +341,64 @@
);
}
- emitGroup(group: GrDiffGroup, sectionEl: HTMLElement) {
+ /**
+ * When the line is hidden behind a context expander, expand it.
+ *
+ * @param lineNum A line number to expand. Using number here because other
+ * special case line numbers are never hidden, so it does not make sense
+ * to expand them.
+ * @param side The side the line number refer to.
+ */
+ unhideLine(lineNum: number, side: Side) {
if (!this._builder) return;
- this._builder.emitGroup(group, sectionEl);
+ const groupIndex = this.$.processor.groups.findIndex(group =>
+ group.containsLine(side, lineNum)
+ );
+ // Cannot unhide a line that is not part of the diff.
+ if (groupIndex < 0) return;
+ const group = this._groups[groupIndex];
+ // If it's already visible, great!
+ if (group.type !== GrDiffGroupType.CONTEXT_CONTROL) return;
+ const lineRange = group.lineRange[side];
+ const lineOffset = lineNum - lineRange.start_line;
+ const newGroups = [];
+ const groups = hideInContextControl(
+ group.contextGroups,
+ 0,
+ lineOffset - 1 - this.prefs.context
+ );
+ // If there is a context group, it will be the first group because we
+ // start hiding from 0 offset
+ if (groups[0].type === GrDiffGroupType.CONTEXT_CONTROL) {
+ newGroups.push(groups.shift()!);
+ }
+ newGroups.push(
+ ...hideInContextControl(
+ groups,
+ lineOffset + 1 + this.prefs.context,
+ // Both ends inclusive, so difference is the offset of the last line.
+ // But we need to pass the first line not to hide, which is the element
+ // after.
+ lineRange.end_line - lineRange.start_line + 1
+ )
+ );
+ this._builder.spliceGroups(groupIndex, 1, ...newGroups);
}
- showContext(newGroups: GrDiffGroup[], sectionEl: HTMLElement) {
+ /**
+ * Replace the provided section by rendering the provided groups.
+ *
+ * @param newGroups The groups to be rendered in the place of the section.
+ * @param sectionEl The context section that should be expanded from.
+ */
+ private rerenderSection(
+ newGroups: readonly GrDiffGroup[],
+ sectionEl: HTMLElement
+ ) {
if (!this._builder) return;
- const groups = this._builder.groups;
- const contextIndex = groups.findIndex(group => group.element === sectionEl);
- groups.splice(contextIndex, 1, ...newGroups);
-
- for (const newGroup of newGroups) {
- this._builder.emitGroup(newGroup, sectionEl);
- }
- if (sectionEl.parentNode) {
- sectionEl.parentNode.removeChild(sectionEl);
- }
+ const contextIndex = this._builder.getIndexOfSection(sectionEl);
+ this._builder.spliceGroups(contextIndex, 1, ...newGroups);
setTimeout(() => fireEvent(this, 'render-content'), 1);
}
@@ -353,20 +419,19 @@
throw Error(`Invalid preference value: ${pref}`);
}
- _getDiffBuilder(
- diff: DiffInfo,
- prefs: DiffPreferencesInfo,
- renderPrefs?: RenderPreferences
- ): GrDiffBuilder {
- if (isNaN(prefs.tab_size) || prefs.tab_size <= 0) {
+ _getDiffBuilder(): GrDiffBuilder {
+ if (!this.diff) {
+ throw Error('Cannot render a diff without DiffInfo.');
+ }
+ if (isNaN(this.prefs.tab_size) || this.prefs.tab_size <= 0) {
this._handlePreferenceError('tab size');
}
- if (isNaN(prefs.line_length) || prefs.line_length <= 0) {
+ if (isNaN(this.prefs.line_length) || this.prefs.line_length <= 0) {
this._handlePreferenceError('diff width');
}
- const localPrefs = {...prefs};
+ const localPrefs = {...this.prefs};
if (this.path === COMMIT_MSG_PATH) {
// override line_length for commit msg the same way as
// in gr-diff
@@ -376,32 +441,32 @@
let builder = null;
if (this.isImageDiff) {
builder = new GrDiffBuilderImage(
- diff,
+ this.diff,
localPrefs,
this.diffElement,
this.baseImage,
this.revisionImage,
- renderPrefs,
+ this.renderPrefs,
this.useNewImageDiffUi
);
- } else if (diff.binary) {
+ } else if (this.diff.binary) {
// If the diff is binary, but not an image.
- return new GrDiffBuilderBinary(diff, localPrefs, this.diffElement);
+ return new GrDiffBuilderBinary(this.diff, localPrefs, this.diffElement);
} else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
builder = new GrDiffBuilderSideBySide(
- diff,
+ this.diff,
localPrefs,
this.diffElement,
this._layers,
- renderPrefs
+ this.renderPrefs
);
} else if (this.viewMode === DiffViewMode.UNIFIED) {
builder = new GrDiffBuilderUnified(
- diff,
+ this.diff,
localPrefs,
this.diffElement,
this._layers,
- renderPrefs
+ this.renderPrefs
);
}
if (!builder) {
@@ -419,13 +484,13 @@
if (!changeRecord || !this._builder) {
return;
}
+ // Forward any splices to the builder
for (const splice of changeRecord.indexSplices) {
- let group;
- for (let i = 0; i < splice.addedCount; i++) {
- group = splice.object[splice.index + i];
- this._builder.groups.push(group);
- this._builder.emitGroup(group, null);
- }
+ const added = splice.object.slice(
+ splice.index,
+ splice.index + splice.addedCount
+ );
+ this._builder.spliceGroups(splice.index, splice.removed.length, ...added);
}
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
index 44b0b8b..0e74dfe 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
@@ -16,9 +16,6 @@
*/
import '../../../test/common-test-setup-karma.js';
-import '../gr-diff/gr-diff-group.js';
-import './gr-diff-builder.js';
-import '../gr-context-controls/gr-context-controls.js';
import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
import './gr-diff-builder-element.js';
import {stubBaseUrl} from '../../../test/test-utils.js';
@@ -29,8 +26,9 @@
import {GrDiffBuilder} from './gr-diff-builder.js';
import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-import {DiffViewMode} from '../../../api/diff.js';
+import {DiffViewMode, Side} from '../../../api/diff.js';
import {stubRestApi} from '../../../test/test-utils.js';
+import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
const basicFixture = fixtureFromTemplate(html`
<gr-diff-builder>
@@ -48,6 +46,14 @@
</gr-diff-builder>
`);
+// GrDiffBuilderElement forces these prefs to be set - tests that do not care
+// about these values can just set these defaults.
+const DEFAULT_PREFS = {
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+};
+
suite('gr-diff-builder tests', () => {
let prefs;
let element;
@@ -61,11 +67,7 @@
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
stubRestApi('getProjectConfig').returns(Promise.resolve({}));
stubBaseUrl('/r');
- prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- };
+ prefs = {...DEFAULT_PREFS};
builder = new GrDiffBuilder({content: []}, prefs);
});
@@ -142,18 +144,18 @@
test(`line_length used for regular files under ${mode}`, () => {
element.path = '/a.txt';
element.viewMode = mode;
- builder = element._getDiffBuilder(
- {}, {tab_size: 4, line_length: 50}
- );
+ element.diff = {};
+ element.prefs = {tab_size: 4, line_length: 50};
+ builder = element._getDiffBuilder();
assert.equal(builder._prefs.line_length, 50);
});
test(`line_length ignored for commit msg under ${mode}`, () => {
element.path = '/COMMIT_MSG';
element.viewMode = mode;
- builder = element._getDiffBuilder(
- {}, {tab_size: 4, line_length: 50}
- );
+ element.diff = {};
+ element.prefs = {tab_size: 4, line_length: 50};
+ builder = element._getDiffBuilder();
assert.equal(builder._prefs.line_length, 72);
});
});
@@ -237,8 +239,8 @@
});
test('_handlePreferenceError throws with invalid preference', () => {
- const prefs = {tab_size: 0};
- assert.throws(() => element._getDiffBuilder(element.diff, prefs));
+ element.prefs = {tab_size: 0};
+ assert.throws(() => element._getDiffBuilder());
});
test('_handlePreferenceError triggers alert and javascript error', () => {
@@ -252,31 +254,34 @@
suite('_isTotal', () => {
test('is total for add', () => {
- const group = new GrDiffGroup(GrDiffGroupType.DELTA);
+ const lines = [];
for (let idx = 0; idx < 10; idx++) {
- group.addLine(new GrDiffLine(GrDiffLineType.ADD));
+ lines.push(new GrDiffLine(GrDiffLineType.ADD));
}
+ const group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
assert.isTrue(GrDiffBuilder.prototype._isTotal(group));
});
test('is total for remove', () => {
- const group = new GrDiffGroup(GrDiffGroupType.DELTA);
+ const lines = [];
for (let idx = 0; idx < 10; idx++) {
- group.addLine(new GrDiffLine(GrDiffLineType.REMOVE));
+ lines.push(new GrDiffLine(GrDiffLineType.REMOVE));
}
+ const group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
assert.isTrue(GrDiffBuilder.prototype._isTotal(group));
});
test('not total for empty', () => {
- const group = new GrDiffGroup(GrDiffGroupType.BOTH);
+ const group = new GrDiffGroup({type: GrDiffGroupType.BOTH});
assert.isFalse(GrDiffBuilder.prototype._isTotal(group));
});
test('not total for non-delta', () => {
- const group = new GrDiffGroup(GrDiffGroupType.DELTA);
+ const lines = [];
for (let idx = 0; idx < 10; idx++) {
- group.addLine(new GrDiffLine(GrDiffLineType.BOTH));
+ lines.push(new GrDiffLine(GrDiffLineType.BOTH));
}
+ const group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
assert.isFalse(GrDiffBuilder.prototype._isTotal(group));
});
});
@@ -693,7 +698,6 @@
suite('rendering text, images and binary files', () => {
let processStub;
let keyLocations;
- let prefs;
let content;
setup(() => {
@@ -702,10 +706,8 @@
processStub = sinon.stub(element.$.processor, 'process')
.returns(Promise.resolve());
keyLocations = {left: {}, right: {}};
- prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
+ element.prefs = {
+ ...DEFAULT_PREFS,
context: -1,
syntax_highlighting: true,
};
@@ -722,7 +724,7 @@
test('text', () => {
element.diff = {content};
- return element.render(keyLocations, prefs).then(() => {
+ return element.render(keyLocations).then(() => {
assert.isTrue(processStub.calledOnce);
assert.isFalse(processStub.lastCall.args[1]);
});
@@ -731,7 +733,7 @@
test('image', () => {
element.diff = {content, binary: true};
element.isImageDiff = true;
- return element.render(keyLocations, prefs).then(() => {
+ return element.render(keyLocations).then(() => {
assert.isTrue(processStub.calledOnce);
assert.isTrue(processStub.lastCall.args[1]);
});
@@ -739,7 +741,7 @@
test('binary', () => {
element.diff = {content, binary: true};
- return element.render(keyLocations, prefs).then(() => {
+ return element.render(keyLocations).then(() => {
assert.isTrue(processStub.calledOnce);
assert.isTrue(processStub.lastCall.args[1]);
});
@@ -752,13 +754,7 @@
let keyLocations;
setup(async () => {
- const prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- context: -1,
- syntax_highlighting: true,
- };
+ const prefs = {...DEFAULT_PREFS};
content = [
{
a: ['all work and no play make andybons a dull boy'],
@@ -772,6 +768,7 @@
},
];
element = basicFixture.instantiate();
+ sinon.stub(element, 'dispatchEvent');
outputEl = element.querySelector('#diffTable');
keyLocations = {left: {}, right: {}};
sinon.stub(element, '_getDiffBuilder').callsFake(() => {
@@ -786,53 +783,134 @@
return builder;
});
element.diff = {content};
- await element.render(keyLocations, prefs);
+ element.prefs = prefs;
+ await element.render(keyLocations);
});
- test('addColumns is called', async () => {
- await element.render(keyLocations, {});
+ test('addColumns is called', () => {
assert.isTrue(element._builder.addColumns.called);
});
- test('getSectionsByLineRange one line', () => {
+ test('getGroupsByLineRange one line', () => {
const section = outputEl.querySelector('stub:nth-of-type(3)');
- const sections = element._builder.getSectionsByLineRange(1, 1, 'left');
- assert.equal(sections.length, 1);
- assert.strictEqual(sections[0], section);
+ const groups = element._builder.getGroupsByLineRange(1, 1, 'left');
+ assert.equal(groups.length, 1);
+ assert.strictEqual(groups[0].element, section);
});
- test('getSectionsByLineRange over diff', () => {
+ test('getGroupsByLineRange over diff', () => {
const section = [
outputEl.querySelector('stub:nth-of-type(3)'),
outputEl.querySelector('stub:nth-of-type(4)'),
];
- const sections = element._builder.getSectionsByLineRange(1, 2, 'left');
- assert.equal(sections.length, 2);
- assert.strictEqual(sections[0], section[0]);
- assert.strictEqual(sections[1], section[1]);
+ const groups = element._builder.getGroupsByLineRange(1, 2, 'left');
+ assert.equal(groups.length, 2);
+ assert.strictEqual(groups[0].element, section[0]);
+ assert.strictEqual(groups[1].element, section[1]);
});
test('render-start and render-content are fired', async () => {
- const dispatchEventStub = sinon.stub(element, 'dispatchEvent');
- await element.render(keyLocations, {});
- const firedEventTypes = dispatchEventStub.getCalls()
+ const firedEventTypes = element.dispatchEvent.getCalls()
.map(c => c.args[0].type);
assert.include(firedEventTypes, 'render-start');
assert.include(firedEventTypes, 'render-content');
});
- test('cancel', () => {
+ test('cancel cancels the processor', () => {
const processorCancelStub = sinon.stub(element.$.processor, 'cancel');
element.cancel();
assert.isTrue(processorCancelStub.called);
});
});
+ suite('context hiding and expanding', () => {
+ setup(async () => {
+ element = basicFixture.instantiate();
+ const afterNextRenderPromise = new Promise((resolve, reject) => {
+ afterNextRender(element, resolve);
+ });
+ element.diff = {
+ content: [
+ {ab: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(i => `unchanged ${i}`)},
+ {a: ['before'], b: ['after']},
+ {ab: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(i => `unchanged ${10 + i}`)},
+ ],
+ };
+ element.viewMode = DiffViewMode.SIDE_BY_SIDE;
+
+ const keyLocations = {left: {}, right: {}};
+ element.prefs = {
+ ...DEFAULT_PREFS,
+ context: 1,
+ };
+ await element.render(keyLocations);
+ // Make sure all listeners are installed.
+ await afterNextRenderPromise;
+ });
+
+ test('hides lines behind two context controls', () => {
+ const contextControls = element.querySelectorAll('gr-context-controls');
+ assert.equal(contextControls.length, 2);
+
+ const diffRows = element.querySelectorAll('.diff-row');
+ // The first two are LOST and FILE line
+ assert.equal(diffRows.length, 2 + 1 + 1 + 1);
+ assert.include(diffRows[2].textContent, 'unchanged 10');
+ assert.include(diffRows[3].textContent, 'before');
+ assert.include(diffRows[3].textContent, 'after');
+ assert.include(diffRows[4].textContent, 'unchanged 11');
+ });
+
+ test('clicking +x common lines expands those lines', () => {
+ const contextControls = element.querySelectorAll('gr-context-controls');
+ const topExpandCommonButton = contextControls[0].shadowRoot
+ .querySelectorAll('.showContext')[0];
+ assert.include(topExpandCommonButton.textContent, '+9 common lines');
+ topExpandCommonButton.click();
+ const diffRows = element.querySelectorAll('.diff-row');
+ // The first two are LOST and FILE line
+ assert.equal(diffRows.length, 2 + 10 + 1 + 1);
+ assert.include(diffRows[2].textContent, 'unchanged 1');
+ assert.include(diffRows[3].textContent, 'unchanged 2');
+ assert.include(diffRows[4].textContent, 'unchanged 3');
+ assert.include(diffRows[5].textContent, 'unchanged 4');
+ assert.include(diffRows[6].textContent, 'unchanged 5');
+ assert.include(diffRows[7].textContent, 'unchanged 6');
+ assert.include(diffRows[8].textContent, 'unchanged 7');
+ assert.include(diffRows[9].textContent, 'unchanged 8');
+ assert.include(diffRows[10].textContent, 'unchanged 9');
+ assert.include(diffRows[11].textContent, 'unchanged 10');
+ assert.include(diffRows[12].textContent, 'before');
+ assert.include(diffRows[12].textContent, 'after');
+ assert.include(diffRows[13].textContent, 'unchanged 11');
+ });
+
+ test('unhideLine shows the line with context', () => {
+ element.unhideLine(4, Side.LEFT);
+
+ const diffRows = element.querySelectorAll('.diff-row');
+ // The first two are LOST and FILE line
+ // Lines 3-5 (Line 4 plus 1 context in each direction) will be expanded
+ // Because context expanders do not hide <3 lines, lines 1-2 will also
+ // be shown.
+ // Lines 6-9 continue to be hidden
+ assert.equal(diffRows.length, 2 + 5 + 1 + 1 + 1);
+ assert.include(diffRows[2].textContent, 'unchanged 1');
+ assert.include(diffRows[3].textContent, 'unchanged 2');
+ assert.include(diffRows[4].textContent, 'unchanged 3');
+ assert.include(diffRows[5].textContent, 'unchanged 4');
+ assert.include(diffRows[6].textContent, 'unchanged 5');
+ assert.include(diffRows[7].textContent, 'unchanged 10');
+ assert.include(diffRows[8].textContent, 'before');
+ assert.include(diffRows[8].textContent, 'after');
+ assert.include(diffRows[9].textContent, 'unchanged 11');
+ });
+ });
+
suite('mock-diff', () => {
let element;
let builder;
let diff;
- let prefs;
let keyLocations;
setup(async () => {
@@ -840,14 +918,14 @@
diff = getMockDiffResponse();
element.diff = diff;
- prefs = {
+ keyLocations = {left: {}, right: {}};
+
+ element.prefs = {
line_length: 80,
show_tabs: true,
tab_size: 4,
};
- keyLocations = {left: {}, right: {}};
-
- await element.render(keyLocations, prefs);
+ await element.render(keyLocations);
builder = element._builder;
});
@@ -985,7 +1063,7 @@
test('_getLineNumberEl unified left', async () => {
// Re-render as unified:
element.viewMode = 'UNIFIED_DIFF';
- await element.render(keyLocations, prefs);
+ await element.render(keyLocations);
builder = element._builder;
const contentEl = builder.getContentByLine(5, 'left',
@@ -998,7 +1076,7 @@
test('_getLineNumberEl unified right', async () => {
// Re-render as unified:
element.viewMode = 'UNIFIED_DIFF';
- await element.render(keyLocations, prefs);
+ await element.render(keyLocations);
builder = element._builder;
const contentEl = builder.getContentByLine(5, 'right',
@@ -1035,7 +1113,7 @@
test('_getNextContentOnSide unified left', async () => {
// Re-render as unified:
element.viewMode = 'UNIFIED_DIFF';
- await element.render(keyLocations, prefs);
+ await element.render(keyLocations);
builder = element._builder;
const startElem = builder.getContentByLine(5, 'left',
@@ -1052,7 +1130,7 @@
test('_getNextContentOnSide unified right', async () => {
// Re-render as unified:
element.viewMode = 'UNIFIED_DIFF';
- await element.render(keyLocations, prefs);
+ await element.render(keyLocations);
builder = element._builder;
const startElem = builder.getContentByLine(5, 'right',
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
index bd7dc29..5d60083 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
@@ -60,11 +60,7 @@
sectionEl.classList.add('ignoredWhitespaceOnly');
}
if (group.type === GrDiffGroupType.CONTEXT_CONTROL) {
- this._createContextControls(
- sectionEl,
- group.contextGroups,
- DiffViewMode.SIDE_BY_SIDE
- );
+ this._createContextControls(sectionEl, group, DiffViewMode.SIDE_BY_SIDE);
return sectionEl;
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts
index a7b3a42..4355e12 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.ts
@@ -59,11 +59,7 @@
sectionEl.classList.add('ignoredWhitespaceOnly');
}
if (group.type === GrDiffGroupType.CONTEXT_CONTROL) {
- this._createContextControls(
- sectionEl,
- group.contextGroups,
- DiffViewMode.UNIFIED
- );
+ this._createContextControls(sectionEl, group, DiffViewMode.UNIFIED);
return sectionEl;
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.js
index 2be85c3..5f3fb72 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.js
@@ -52,7 +52,7 @@
lines[1].text = ' print "Hello World";';
lines[2].text = ' return True';
- group = new GrDiffGroup(GrDiffGroupType.BOTH, lines);
+ group = new GrDiffGroup({type: GrDiffGroupType.BOTH, lines});
});
test('creates the section', () => {
@@ -104,7 +104,7 @@
];
lines[0].text = 'def hello_world():';
lines[1].text = ' print "Hello World"';
- const group = new GrDiffGroup(GrDiffGroupType.DELTA, lines);
+ const group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
group.moveDetails = {changed: false};
const sectionEl = diffBuilder.buildSectionElement(group);
@@ -127,7 +127,7 @@
];
lines[0].text = 'def hello_world():';
lines[1].text = ' print "Hello World"';
- const group = new GrDiffGroup(GrDiffGroupType.DELTA, lines);
+ const group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
group.moveDetails = {changed: false};
const sectionEl = diffBuilder.buildSectionElement(group);
@@ -160,7 +160,7 @@
lines[2].text = 'def hello_universe()';
lines[3].text = ' print "Hello Universe"';
- group = new GrDiffGroup(GrDiffGroupType.DELTA, lines);
+ group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
});
test('creates the section', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
index 663ee7e..28172e5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
@@ -95,6 +95,15 @@
);
}
+/**
+ * Base class for different diff builders, like side-by-side, unified etc.
+ *
+ * The builder takes GrDiffGroups, and builds the corresponding DOM elements,
+ * called sections. Only the builder should add or remove sections from the
+ * DOM. Callers can use the spliceGroups method to add groups that
+ * will then be rendered - or remove groups whose sections will then be
+ * removed from the DOM.
+ */
export abstract class GrDiffBuilder {
private readonly _diff: DiffInfo;
@@ -106,7 +115,7 @@
protected readonly _outputEl: HTMLElement;
- readonly groups: GrDiffGroup[];
+ protected readonly groups: GrDiffGroup[];
private blameInfo: BlameInfo[] | null;
@@ -181,45 +190,58 @@
abstract buildSectionElement(group: GrDiffGroup): HTMLElement;
- emitGroup(group: GrDiffGroup, beforeSection: HTMLElement | null) {
+ getIndexOfSection(sectionEl: HTMLElement) {
+ return this.groups.findIndex(group => group.element === sectionEl);
+ }
+
+ spliceGroups(
+ start: number,
+ deleteCount: number,
+ ...addedGroups: GrDiffGroup[]
+ ) {
+ const sectionBeforeWhichToInsert =
+ start < this.groups.length ? this.groups[start].element ?? null : null;
+ // Update the groups array
+ const deletedGroups = this.groups.splice(
+ start,
+ deleteCount,
+ ...addedGroups
+ );
+
+ // Add new sections for the new groups
+ for (const addedGroup of addedGroups) {
+ this.emitGroup(addedGroup, sectionBeforeWhichToInsert);
+ }
+ // Remove sections corresponding to deleted groups from the DOM
+ for (const deletedGroup of deletedGroups) {
+ const section = deletedGroup.element;
+ section?.parentNode?.removeChild(section);
+ }
+ return deletedGroups;
+ }
+
+ private emitGroup(group: GrDiffGroup, beforeSection: HTMLElement | null) {
const element = this.buildSectionElement(group);
this._outputEl.insertBefore(element, beforeSection);
group.element = element;
}
- getGroupsByLineRange(
+ private getGroupsByLineRange(
startLine: LineNumber,
endLine: LineNumber,
- side?: Side
+ side: Side
) {
- const groups = [];
- for (let i = 0; i < this.groups.length; i++) {
- const group = this.groups[i];
- if (group.lines.length === 0) {
- continue;
- }
- let groupStartLine = 0;
- let groupEndLine = 0;
- if (side) {
- const range =
- side === Side.LEFT ? group.lineRange.left : group.lineRange.right;
- groupStartLine = range.start_line;
- groupEndLine = range.end_line;
- }
-
- if (groupStartLine === 0) {
- // Line was removed or added.
- groupStartLine = groupEndLine;
- }
- if (groupEndLine === 0) {
- // Line was removed or added.
- groupEndLine = groupStartLine;
- }
- if (startLine <= groupEndLine && endLine >= groupStartLine) {
- groups.push(group);
- }
- }
- return groups;
+ const startIndex = this.groups.findIndex(group =>
+ group.containsLine(side, startLine)
+ );
+ const endIndex = this.groups.findIndex(group =>
+ group.containsLine(side, endLine)
+ );
+ // The filter preserves the legacy behavior to only return non-context
+ // groups
+ return this.groups
+ .slice(startIndex, endIndex + 1)
+ .filter(group => group.lines.length > 0);
}
getContentTdByLine(
@@ -318,26 +340,16 @@
}
}
- getSectionsByLineRange(
- startLine: LineNumber,
- endLine: LineNumber,
- side: Side
- ) {
- return this.getGroupsByLineRange(startLine, endLine, side).map(
- group => group.element
- );
- }
-
_createContextControls(
section: HTMLElement,
- contextGroups: GrDiffGroup[],
+ group: GrDiffGroup,
viewMode: DiffViewMode
) {
- const leftStart = contextGroups[0].lineRange.left.start_line;
- const leftEnd =
- contextGroups[contextGroups.length - 1].lineRange.left.end_line;
- const firstGroupIsSkipped = !!contextGroups[0].skip;
- const lastGroupIsSkipped = !!contextGroups[contextGroups.length - 1].skip;
+ const leftStart = group.lineRange.left.start_line;
+ const leftEnd = group.lineRange.left.end_line;
+ const firstGroupIsSkipped = !!group.contextGroups[0].skip;
+ const lastGroupIsSkipped =
+ !!group.contextGroups[group.contextGroups.length - 1].skip;
const containsWholeFile = this._numLinesLeft === leftEnd - leftStart + 1;
const showAbove =
@@ -352,7 +364,7 @@
section.appendChild(
this._createContextControlRow(
section,
- contextGroups,
+ group,
showAbove,
showBelow,
viewMode
@@ -371,7 +383,7 @@
*/
_createContextControlRow(
section: HTMLElement,
- contextGroups: GrDiffGroup[],
+ group: GrDiffGroup,
showAbove: boolean,
showBelow: boolean,
viewMode: DiffViewMode
@@ -405,7 +417,7 @@
contextControls.diff = this._diff;
contextControls.renderPreferences = this._renderPrefs;
contextControls.section = section;
- contextControls.contextGroups = contextGroups;
+ contextControls.group = group;
contextControls.showConfig = showConfig;
cell.appendChild(contextControls);
return row;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
index de7d007..54b2450f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer.ts
@@ -198,7 +198,6 @@
element,
} = this.findTokenAncestor(e?.target);
if (!newHighlight || newHighlight === this.currentHighlight) return;
- if (this.countOccurrences(newHighlight) <= 1) return;
this.hoveredElement = element;
this.updateTokenTask = debounce(
this.updateTokenTask,
@@ -247,13 +246,6 @@
return this.findTokenAncestor(el.parentElement);
}
- countOccurrences(token: string | undefined) {
- if (!token) return 0;
- const linesLeft = this.tokenToLinesLeft.get(token);
- const linesRight = this.tokenToLinesRight.get(token);
- return (linesLeft?.size ?? 0) + (linesRight?.size ?? 0);
- }
-
private updateTokenHighlight(
newHighlight: string | undefined,
newLineNumber: number,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
index 2993d35..a0670b8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/token-highlight-layer_test.ts
@@ -290,6 +290,34 @@
assert.deepEqual(tokenHighlightingCalls[1].details, undefined);
});
+ test('triggers listener on token with single occurrence', async () => {
+ const clock = sinon.useFakeTimers();
+ const line1 = createLine('a tokenWithSingleOccurence');
+ const line2 = createLine('can be highlighted', 2);
+ annotate(line1);
+ annotate(line2, Side.RIGHT, 2);
+ const tokenNode = queryAndAssert(line1, '.tk-tokenWithSingleOccurence');
+ assert.isTrue(tokenNode.classList.contains('token'));
+ dispatchMouseEvent(
+ 'mouseover',
+ MockInteractions.middleOfNode(tokenNode),
+ tokenNode
+ );
+ assert.equal(tokenHighlightingCalls.length, 0);
+ clock.tick(HOVER_DELAY_MS);
+ assert.equal(tokenHighlightingCalls.length, 1);
+ assert.deepEqual(tokenHighlightingCalls[0].details, {
+ token: 'tokenWithSingleOccurence',
+ side: Side.RIGHT,
+ element: tokenNode,
+ range: {start_line: 1, start_column: 3, end_line: 1, end_column: 26},
+ });
+
+ MockInteractions.click(container);
+ assert.equal(tokenHighlightingCalls.length, 2);
+ assert.deepEqual(tokenHighlightingCalls[1].details, undefined);
+ });
+
test('clicking clears highlight', async () => {
const clock = sinon.useFakeTimers();
const line1 = createLine('two words');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts
index 958f367..89ab885 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts
@@ -222,11 +222,16 @@
return result;
}
- moveToLineNumber(number: number, side: Side, path?: string) {
+ moveToLineNumber(
+ number: number,
+ side: Side,
+ path?: string,
+ intentionalMove?: boolean
+ ) {
const row = this._findRowByNumberAndFile(number, side, path);
if (row) {
this.side = side;
- this.cursorManager.setCursor(row);
+ this.cursorManager.setCursor(row, undefined, intentionalMove);
}
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.ts b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.ts
index 3e292fe..a47db20 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.ts
@@ -408,6 +408,7 @@
range: Text | Element | Range
) {
if (startLine > 1) {
+ actionBox.positionBelow = false;
actionBox.placeAbove(range);
return;
}
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 59bd817..de439cd 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
@@ -16,7 +16,6 @@
*/
import '../../shared/gr-comment-thread/gr-comment-thread';
import '../gr-diff/gr-diff';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-diff-host_html';
import {
GerritNav,
@@ -25,19 +24,22 @@
import {
anyLineTooLong,
getLine,
- getRange,
getSide,
- rangesEqual,
SYNTAX_MAX_LINE_LENGTH,
} from '../gr-diff/gr-diff-utils';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {
getParentIndex,
isAParent,
isMergeParent,
isNumber,
} from '../../../utils/patch-set-util';
-import {CommentThread} from '../../../utils/comment-util';
+import {
+ CommentThread,
+ equalLocation,
+ isInBaseOfPatchRange,
+ isInRevisionOfPatchRange,
+} from '../../../utils/comment-util';
import {customElement, observe, property} from '@polymer/decorators';
import {
CommitRange,
@@ -63,11 +65,7 @@
DiffPreferencesInfo,
IgnoreWhitespaceType,
} from '../../../types/diff';
-import {
- CreateCommentEventDetail,
- GrDiff,
- LineOfInterest,
-} from '../gr-diff/gr-diff';
+import {CreateCommentEventDetail, GrDiff} from '../gr-diff/gr-diff';
import {GrSyntaxLayer} from '../gr-syntax-layer/gr-syntax-layer';
import {DiffViewMode, Side, CommentSide} from '../../../constants/constants';
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
@@ -87,11 +85,12 @@
import {DiffContextExpandedEventDetail} from '../gr-diff-builder/gr-diff-builder';
import {TokenHighlightLayer} from '../gr-diff-builder/token-highlight-layer';
import {Timing} from '../../../constants/reporting';
-import {changeComments$} from '../../../services/comments/comments-model';
-import {takeUntil} from 'rxjs/operators';
import {ChangeComments} from '../gr-comment-api/gr-comment-api';
-import {Subject} from 'rxjs';
-import {RenderPreferences} from '../../../api/diff';
+import {Subscription} from 'rxjs';
+import {DisplayLine, RenderPreferences} from '../../../api/diff';
+import {resolve, DIPolymerElement} from '../../../models/dependency';
+import {browserModelToken} from '../../../models/browser/browser-model';
+import {commentsModelToken} from '../../../models/comments/comments-model';
const EMPTY_BLAME = 'No blame information for this diff.';
@@ -133,7 +132,7 @@
* specific component, while <gr-diff> is a re-usable component.
*/
@customElement('gr-diff-host')
-export class GrDiffHost extends PolymerElement {
+export class GrDiffHost extends DIPolymerElement {
static get template() {
return htmlTemplate;
}
@@ -205,12 +204,12 @@
@property({type: Boolean})
lineWrapping = false;
+ @property({type: Object})
+ lineOfInterest?: DisplayLine;
+
@property({type: String})
viewMode = DiffViewMode.SIDE_BY_SIDE;
- @property({type: Object})
- lineOfInterest?: LineOfInterest;
-
@property({type: Boolean})
showLoadFailure?: boolean;
@@ -269,17 +268,21 @@
num_lines_rendered_at_once: 128,
};
- private readonly reporting = appContext.reportingService;
+ private readonly getBrowserModel = resolve(this, browserModelToken);
- private readonly flags = appContext.flagsService;
+ private readonly getCommentsModel = resolve(this, commentsModelToken);
- private readonly restApiService = appContext.restApiService;
+ private readonly reporting = getAppContext().reportingService;
- private readonly jsAPI = appContext.jsApiService;
+ private readonly flags = getAppContext().flagsService;
+
+ private readonly restApiService = getAppContext().restApiService;
+
+ private readonly jsAPI = getAppContext().jsApiService;
private readonly syntaxLayer = new GrSyntaxLayer();
- disconnected$ = new Subject();
+ private subscriptions: Subscription[] = [];
constructor() {
super();
@@ -291,7 +294,7 @@
// change in some way, and that we should update any models we may want
// to keep in sync.
'create-comment',
- e => this._handleCreateComment(e)
+ e => this._handleCreateThread(e)
);
this.addEventListener('render-start', () => this._handleRenderStart());
this.addEventListener('render-content', () => this._handleRenderContent());
@@ -312,24 +315,32 @@
override connectedCallback() {
super.connectedCallback();
+ this.subscriptions.push(
+ this.getBrowserModel().diffViewMode$.subscribe(
+ diffView => (this.viewMode = diffView)
+ )
+ );
this._getLoggedIn().then(loggedIn => {
this._loggedIn = loggedIn;
});
- changeComments$
- .pipe(takeUntil(this.disconnected$))
- .subscribe(changeComments => {
+ this.subscriptions.push(
+ this.getCommentsModel().changeComments$.subscribe(changeComments => {
this.changeComments = changeComments;
- });
+ })
+ );
}
override disconnectedCallback() {
- this.disconnected$.next();
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions = [];
this.clear();
super.disconnectedCallback();
}
async initLayers() {
- const preferencesPromise = appContext.restApiService.getPreferences();
+ const preferencesPromise = this.restApiService.getPreferences();
await getPluginLoader().awaitPluginsLoaded();
const prefs = await preferencesPromise;
const enableTokenHighlight = !prefs?.disable_token_highlighting;
@@ -401,11 +412,13 @@
this.reporting.timeEnd(Timing.DIFF_SYNTAX);
}
}
- } catch (e) {
+ } catch (e: unknown) {
if (e instanceof Response) {
this._handleGetDiffError(e);
- } else {
+ } else if (e instanceof Error) {
this.reporting.error(e);
+ } else {
+ this.reporting.error(new Error('reload error'), undefined, e);
}
} finally {
this.reporting.timeEnd(Timing.DIFF_TOTAL);
@@ -729,30 +742,54 @@
}
_threadsChanged(threads: CommentThread[]) {
- const threadEls = new Set<GrCommentThread>();
const rootIdToThreadEl = new Map<UrlEncodedCommentId, GrCommentThread>();
+ const unsavedThreadEls: GrCommentThread[] = [];
for (const threadEl of this.getThreadEls()) {
if (threadEl.rootId) {
rootIdToThreadEl.set(threadEl.rootId, threadEl);
+ } else {
+ // Unsaved thread els must have editing:true, just being defensive here.
+ if (threadEl.editing) unsavedThreadEls.push(threadEl);
}
}
+ const dontRemove = new Set<GrCommentThread>();
for (const thread of threads) {
- const existingThreadEl =
+ // Let's find an existing DOM element matching the thread. Normally this
+ // is as simple as matching the rootIds.
+ let existingThreadEl =
thread.rootId && rootIdToThreadEl.get(thread.rootId);
+ // But unsaved threads don't have rootIds. The incoming thread might be
+ // the saved version of the unsaved thread element. To verify that we
+ // check that the thread only has one comment and that their location is
+ // identical.
+ // TODO(brohlfs): This matching is not perfect. You could quickly create
+ // two new threads on the same line/range. Then this code just makes a
+ // random guess.
+ if (!existingThreadEl && thread.comments?.length === 1) {
+ for (const unsavedThreadEl of unsavedThreadEls) {
+ if (equalLocation(unsavedThreadEl.thread, thread)) {
+ existingThreadEl = unsavedThreadEl;
+ break;
+ }
+ }
+ }
if (existingThreadEl) {
- this._updateThreadElement(existingThreadEl, thread);
- threadEls.add(existingThreadEl);
+ existingThreadEl.thread = thread;
+ dontRemove.add(existingThreadEl);
} else {
const threadEl = this._createThreadElement(thread);
this._attachThreadElement(threadEl);
- threadEls.add(threadEl);
+ dontRemove.add(threadEl);
}
}
// Remove all threads that are no longer existing.
for (const threadEl of this.getThreadEls()) {
- if (threadEls.has(threadEl)) continue;
- const parent = threadEl.parentNode;
- if (parent) parent.removeChild(threadEl);
+ if (dontRemove.has(threadEl)) continue;
+ // The user may have opened a couple of comment boxes for editing. They
+ // might be unsaved and thus not be reflected in `threads` yet, so let's
+ // keep them open.
+ if (threadEl.editing && threadEl.thread?.comments.length === 0) continue;
+ threadEl.remove();
}
const portedThreadsCount = threads.filter(thread => thread.ported).length;
const portedThreadsWithoutRange = threads.filter(
@@ -780,10 +817,10 @@
);
}
- _handleCreateComment(e: CustomEvent<CreateCommentEventDetail>) {
+ _handleCreateThread(e: CustomEvent<CreateCommentEventDetail>) {
if (!this.patchRange) throw Error('patch range not set');
- const {lineNum, side, range, path} = e.detail;
+ const {lineNum, side, range} = e.detail;
// Usually, the comment is stored on the patchset shown on the side the
// user added the comment on, and the commentSide will be REVISION.
@@ -801,18 +838,27 @@
? CommentSide.PARENT
: CommentSide.REVISION;
if (!this.canCommentOnPatchSetNum(patchNum)) return;
- const threadEl = this._getOrCreateThread({
+ const path =
+ this.file?.basePath &&
+ side === Side.LEFT &&
+ commentSide === CommentSide.REVISION
+ ? this.file?.basePath
+ : this.path;
+ assertIsDefined(path, 'path');
+
+ const newThread: CommentThread = {
+ rootId: undefined,
comments: [],
- path,
- diffSide: side,
- commentSide,
patchNum,
+ commentSide,
+ // TODO: Maybe just compute from patchRange.base on the fly?
+ mergeParentNum: this._parentIndex ?? undefined,
+ path,
line: lineNum,
range,
- });
- threadEl.addOrEditDraft(lineNum, range);
-
- this.reporting.recordDraftInteraction();
+ };
+ const el = this._createThreadElement(newThread);
+ this._attachThreadElement(el);
}
private canCommentOnPatchSetNum(patchNum: PatchSetNum) {
@@ -841,21 +887,6 @@
return true;
}
- /**
- * Gets or creates a comment thread at a given location.
- * May provide a range, to get/create a range comment.
- */
- _getOrCreateThread(thread: CommentThread): GrCommentThread {
- let threadEl = this._getThreadEl(thread);
- if (!threadEl) {
- threadEl = this._createThreadElement(thread);
- this._attachThreadElement(threadEl);
- } else {
- this._updateThreadElement(threadEl, thread);
- }
- return threadEl;
- }
-
_attachThreadElement(threadEl: Element) {
this.$.diff.appendChild(threadEl);
}
@@ -868,67 +899,38 @@
}
_createThreadElement(thread: CommentThread) {
+ assertIsDefined(this.patchRange, 'patchRange');
+ const commentProps = {
+ patch_set: thread.patchNum,
+ side: thread.commentSide,
+ parent: thread.mergeParentNum,
+ };
+ let diffSide: Side;
+ if (isInBaseOfPatchRange(commentProps, this.patchRange)) {
+ diffSide = Side.LEFT;
+ } else if (isInRevisionOfPatchRange(commentProps, this.patchRange)) {
+ diffSide = Side.RIGHT;
+ } else {
+ const propsStr = JSON.stringify(commentProps);
+ const rangeStr = JSON.stringify(this.patchRange);
+ throw new Error(`comment ${propsStr} not in range ${rangeStr}`);
+ }
+
const threadEl = document.createElement('gr-comment-thread');
threadEl.className = 'comment-thread';
- threadEl.setAttribute(
- 'slot',
- `${thread.diffSide}-${thread.line || 'LOST'}`
- );
- this._updateThreadElement(threadEl, thread);
- return threadEl;
- }
-
- _updateThreadElement(threadEl: GrCommentThread, thread: CommentThread) {
- threadEl.comments = thread.comments;
- threadEl.diffSide = thread.diffSide;
- threadEl.isOnParent = thread.commentSide === CommentSide.PARENT;
- threadEl.parentIndex = this._parentIndex;
- // Use path before renmaing when comment added on the left when comparing
- // two patch sets (not against base)
- if (
- this.file &&
- this.file.basePath &&
- thread.diffSide === Side.LEFT &&
- !threadEl.isOnParent
- ) {
- threadEl.path = this.file.basePath;
- } else {
- threadEl.path = this.path;
- }
- threadEl.changeNum = this.changeNum;
- threadEl.patchNum = thread.patchNum;
+ threadEl.rootId = thread.rootId;
+ threadEl.thread = thread;
threadEl.showPatchset = false;
threadEl.showPortedComment = !!thread.ported;
- if (thread.rangeInfoLost) threadEl.lineNum = 'LOST';
- // GrCommentThread does not understand 'FILE', but requires undefined.
- else threadEl.lineNum = thread.line !== 'FILE' ? thread.line : undefined;
- threadEl.projectName = this.projectName;
- threadEl.range = thread.range;
- }
-
- /**
- * Gets a comment thread element at a given location.
- * May provide a range, to get a range comment.
- */
- _getThreadEl(thread: CommentThread): GrCommentThread | null {
- let line: LineInfo;
- if (thread.diffSide === Side.LEFT) {
- line = {beforeNumber: thread.line};
- } else if (thread.diffSide === Side.RIGHT) {
- line = {afterNumber: thread.line};
- } else {
- throw new Error(`Unknown side: ${thread.diffSide}`);
+ // These attributes are the "interface" between comment threads and gr-diff.
+ // <gr-comment-thread> does not care about them and is not affected by them.
+ threadEl.setAttribute('slot', `${diffSide}-${thread.line || 'LOST'}`);
+ threadEl.setAttribute('diff-side', `${diffSide}`);
+ threadEl.setAttribute('line-num', `${thread.line || 'LOST'}`);
+ if (thread.range) {
+ threadEl.setAttribute('range', `${JSON.stringify(thread.range)}`);
}
- function matchesRange(threadEl: GrCommentThread) {
- return rangesEqual(getRange(threadEl), thread.range);
- }
-
- const filteredThreadEls = this._filterThreadElsForLocation(
- this.getThreadEls(),
- line,
- thread.diffSide
- ).filter(matchesRange);
- return filteredThreadEls.length ? filteredThreadEls[0] : null;
+ return threadEl;
}
_filterThreadElsForLocation(
@@ -1176,8 +1178,6 @@
'normalize-range': CustomEvent;
'diff-context-expanded': CustomEvent<DiffContextExpandedEventDetail>;
'create-comment': CustomEvent;
- 'comment-update': CustomEvent;
- 'comment-save': CustomEvent;
'root-id-changed': CustomEvent;
}
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
index 6facdca..888dce3 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
@@ -20,7 +20,6 @@
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {createDefaultDiffPrefs, Side} from '../../../constants/constants.js';
-import {_testOnly_resetState} from '../../../services/comments/comments-model.js';
import {createChange, createComment, createCommentThread} from '../../../test/test-data-generators.js';
import {addListenerForTest, mockPromise, stubRestApi} from '../../../test/test-utils.js';
import {EditPatchSetNum, ParentPatchSetNum} from '../../../types/common.js';
@@ -43,7 +42,6 @@
element.path = 'some/path';
sinon.stub(element.reporting, 'time');
sinon.stub(element.reporting, 'timeEnd');
- _testOnly_resetState();
await flush();
});
@@ -838,7 +836,7 @@
});
test('passes in lineOfInterest', () => {
- const value = {number: 123, leftSide: true};
+ const value = {lineNum: 123, side: Side.LEFT};
element.lineOfInterest = value;
assert.equal(element.$.diff.lineOfInterest, value);
});
@@ -950,7 +948,6 @@
});
test('creates comments if they do not exist yet', () => {
- const diffSide = Side.LEFT;
element.patchRange = {
basePatchNum: 'PARENT',
patchNum: 2,
@@ -959,7 +956,7 @@
element.dispatchEvent(new CustomEvent('create-comment', {
detail: {
lineNum: 3,
- side: diffSide,
+ side: Side.LEFT,
path: '/p',
},
}));
@@ -968,10 +965,10 @@
.queryDistributedElements('gr-comment-thread');
assert.equal(threads.length, 1);
- assert.equal(threads[0].diffSide, diffSide);
- assert.isTrue(threads[0].isOnParent);
- assert.equal(threads[0].range, undefined);
- assert.equal(threads[0].patchNum, 2);
+ assert.equal(threads[0].thread.commentSide, 'PARENT');
+ assert.equal(threads[0].getAttribute('diff-side'), Side.LEFT);
+ assert.equal(threads[0].thread.range, undefined);
+ assert.equal(threads[0].thread.patchNum, 2);
// Try to fetch a thread with a different range.
const range = {
@@ -988,7 +985,7 @@
element.dispatchEvent(new CustomEvent('create-comment', {
detail: {
lineNum: 1,
- side: diffSide,
+ side: Side.LEFT,
path: '/p',
range,
},
@@ -998,10 +995,10 @@
.queryDistributedElements('gr-comment-thread');
assert.equal(threads.length, 2);
- assert.equal(threads[1].diffSide, diffSide);
- assert.isTrue(threads[0].isOnParent);
- assert.equal(threads[1].range, range);
- assert.equal(threads[1].patchNum, 3);
+ assert.equal(threads[0].thread.commentSide, 'PARENT');
+ assert.equal(threads[0].getAttribute('diff-side'), Side.LEFT);
+ assert.equal(threads[1].thread.range, range);
+ assert.equal(threads[1].thread.patchNum, 3);
});
test('should not be on parent if on the right', () => {
@@ -1016,10 +1013,11 @@
},
}));
- const thread = dom(element.$.diff)
+ const threadEl = dom(element.$.diff)
.queryDistributedElements('gr-comment-thread')[0];
- assert.isFalse(thread.isOnParent);
+ assert.equal(threadEl.thread.commentSide, 'REVISION');
+ assert.equal(threadEl.getAttribute('diff-side'), Side.RIGHT);
});
test('should be on parent if right and base is PARENT', () => {
@@ -1034,10 +1032,11 @@
},
}));
- const thread = dom(element.$.diff)
+ const threadEl = dom(element.$.diff)
.queryDistributedElements('gr-comment-thread')[0];
- assert.isTrue(thread.isOnParent);
+ assert.equal(threadEl.thread.commentSide, 'PARENT');
+ assert.equal(threadEl.getAttribute('diff-side'), Side.LEFT);
});
test('should be on parent if right and base negative', () => {
@@ -1052,10 +1051,11 @@
},
}));
- const thread = dom(element.$.diff)
+ const threadEl = dom(element.$.diff)
.queryDistributedElements('gr-comment-thread')[0];
- assert.isTrue(thread.isOnParent);
+ assert.equal(threadEl.thread.commentSide, 'PARENT');
+ assert.equal(threadEl.getAttribute('diff-side'), Side.LEFT);
});
test('should not be on parent otherwise', () => {
@@ -1070,24 +1070,25 @@
},
}));
- const thread = dom(element.$.diff)
+ const threadEl = dom(element.$.diff)
.queryDistributedElements('gr-comment-thread')[0];
- assert.isFalse(thread.isOnParent);
+ assert.equal(threadEl.thread.commentSide, 'REVISION');
+ assert.equal(threadEl.getAttribute('diff-side'), Side.LEFT);
});
test('thread should use old file path if first created ' +
- 'on patch set (left) before renaming', () => {
- const diffSide = Side.LEFT;
+ 'on patch set (left) before renaming', async () => {
element.patchRange = {
basePatchNum: 2,
patchNum: 3,
};
element.file = {basePath: 'file_renamed.txt', path: element.path};
+ await flush();
element.dispatchEvent(new CustomEvent('create-comment', {
detail: {
- side: diffSide,
+ side: Side.LEFT,
path: '/p',
},
}));
@@ -1096,22 +1097,22 @@
.queryDistributedElements('gr-comment-thread');
assert.equal(threads.length, 1);
- assert.equal(threads[0].diffSide, diffSide);
- assert.equal(threads[0].path, element.file.basePath);
+ assert.equal(threads[0].getAttribute('diff-side'), Side.LEFT);
+ assert.equal(threads[0].thread.path, element.file.basePath);
});
- test('thread should use new file path if first created' +
- 'on patch set (right) after renaming', () => {
- const diffSide = Side.RIGHT;
+ test('thread should use new file path if first created ' +
+ 'on patch set (right) after renaming', async () => {
element.patchRange = {
basePatchNum: 2,
patchNum: 3,
};
element.file = {basePath: 'file_renamed.txt', path: element.path};
+ await flush();
element.dispatchEvent(new CustomEvent('create-comment', {
detail: {
- side: diffSide,
+ side: Side.RIGHT,
path: '/p',
},
}));
@@ -1120,23 +1121,27 @@
.queryDistributedElements('gr-comment-thread');
assert.equal(threads.length, 1);
- assert.equal(threads[0].diffSide, diffSide);
- assert.equal(threads[0].path, element.file.path);
+ assert.equal(threads[0].getAttribute('diff-side'), Side.RIGHT);
+ assert.equal(threads[0].thread.path, element.file.path);
});
- test('multiple threads created on the same range', () => {
+ test('multiple threads created on the same range', async () => {
element.patchRange = {
basePatchNum: 2,
patchNum: 3,
};
element.file = {basePath: 'file_renamed.txt', path: element.path};
+ await flush();
- const comment = createComment();
- comment.range = {
- start_line: 1,
- start_character: 1,
- end_line: 2,
- end_character: 2,
+ const comment = {
+ ...createComment(),
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 2,
+ },
+ patch_set: 3,
};
const thread = createCommentThread([comment]);
element.threads = [thread];
@@ -1161,18 +1166,59 @@
assert.equal(threads.length, 2);
});
- test('thread should use new file path if first created' +
- 'on patch set (left) but is base', () => {
- const diffSide = Side.LEFT;
+ test('unsaved thread changes to draft', async () => {
+ element.patchRange = {
+ basePatchNum: 2,
+ patchNum: 3,
+ };
+ element.file = {basePath: 'file_renamed.txt', path: element.path};
+ element.threads = [];
+ await flush();
+
+ element.dispatchEvent(new CustomEvent('create-comment', {
+ detail: {
+ side: Side.RIGHT,
+ path: element.path,
+ lineNum: 13,
+ },
+ }));
+ await flush();
+ assert.equal(element.getThreadEls().length, 1);
+ const threadEl = element.getThreadEls()[0];
+ assert.equal(threadEl.thread.line, 13);
+ assert.isDefined(threadEl.unsavedComment);
+ assert.equal(threadEl.thread.comments.length, 0);
+
+ const draftThread = createCommentThread([{
+ path: element.path,
+ patch_set: 3,
+ line: 13,
+ __draft: true,
+ }]);
+ element.threads = [draftThread];
+ await flush();
+
+ // We expect that no additional thread element was created.
+ assert.equal(element.getThreadEls().length, 1);
+ // In fact the thread element must still be the same.
+ assert.equal(element.getThreadEls()[0], threadEl);
+ // But it must have been updated from unsaved to draft:
+ assert.isUndefined(threadEl.unsavedComment);
+ assert.equal(threadEl.thread.comments.length, 1);
+ });
+
+ test('thread should use new file path if first created ' +
+ 'on patch set (left) but is base', async () => {
element.patchRange = {
basePatchNum: 'PARENT',
patchNum: 3,
};
element.file = {basePath: 'file_renamed.txt', path: element.path};
+ await flush();
element.dispatchEvent(new CustomEvent('create-comment', {
detail: {
- side: diffSide,
+ side: Side.LEFT,
path: '/p',
},
}));
@@ -1181,8 +1227,8 @@
dom(element.$.diff).queryDistributedElements('gr-comment-thread');
assert.equal(threads.length, 1);
- assert.equal(threads[0].diffSide, diffSide);
- assert.equal(threads[0].path, element.file.path);
+ assert.equal(threads[0].getAttribute('diff-side'), Side.LEFT);
+ assert.equal(threads[0].thread.path, element.file.path);
});
test('cannot create thread on an edit', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
index 32f5f39..290773b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-image-viewer/gr-image-viewer.ts
@@ -30,6 +30,7 @@
import {css, html, LitElement, PropertyValues} from 'lit';
import {customElement, property, query, state} from 'lit/decorators';
+import {ifDefined} from 'lit/directives/if-defined';
import {classMap} from 'lit/directives/class-map';
import {StyleInfo, styleMap} from 'lit/directives/style-map';
@@ -435,7 +436,7 @@
// layer.
'pointer-events': this.showHighlight ? 'auto' : 'none',
})}"
- src="${this.diffHighlightSrc}"
+ src="${ifDefined(this.diffHighlightSrc)}"
/>
</div>
`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
index b47c51c..bfb2bce 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
@@ -14,27 +14,29 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Subscription} from 'rxjs';
import '@polymer/iron-icon/iron-icon';
import '@polymer/iron-a11y-announcer/iron-a11y-announcer';
import '../../../styles/shared-styles';
import '../../shared/gr-button/gr-button';
import {DiffViewMode} from '../../../constants/constants';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-diff-mode-selector_html';
import {customElement, property} from '@polymer/decorators';
import {IronA11yAnnouncer} from '@polymer/iron-a11y-announcer/iron-a11y-announcer';
import {FixIronA11yAnnouncer} from '../../../types/types';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {fireIronAnnounce} from '../../../utils/event-util';
+import {browserModelToken} from '../../../models/browser/browser-model';
+import {resolve, DIPolymerElement} from '../../../models/dependency';
@customElement('gr-diff-mode-selector')
-export class GrDiffModeSelector extends PolymerElement {
+export class GrDiffModeSelector extends DIPolymerElement {
static get template() {
return htmlTemplate;
}
@property({type: String, notify: true})
- mode?: DiffViewMode;
+ mode: DiffViewMode = DiffViewMode.SIDE_BY_SIDE;
/**
* If set to true, the user's preference will be updated every time a
@@ -46,13 +48,35 @@
@property({type: Boolean})
showTooltipBelow = false;
- private readonly userService = appContext.userService;
+ // Private but accessed by tests.
+ readonly getBrowserModel = resolve(this, browserModelToken);
+
+ private readonly userModel = getAppContext().userModel;
+
+ private subscriptions: Subscription[] = [];
+
+ constructor() {
+ super();
+ }
override connectedCallback() {
super.connectedCallback();
(
IronA11yAnnouncer as unknown as FixIronA11yAnnouncer
).requestAvailability();
+ this.subscriptions.push(
+ this.getBrowserModel().diffViewMode$.subscribe(
+ diffView => (this.mode = diffView)
+ )
+ );
+ }
+
+ override disconnectedCallback() {
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions = [];
+ super.disconnectedCallback();
}
/**
@@ -60,7 +84,7 @@
*/
setMode(newMode: DiffViewMode) {
if (this.saveOnChange && this.mode && this.mode !== newMode) {
- this.userService.updatePreferences({diff_view: newMode});
+ this.userModel.updatePreferences({diff_view: newMode});
}
this.mode = newMode;
let announcement;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
index 8b06c75..3ade907 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
@@ -47,8 +47,10 @@
});
test('setMode', () => {
+ element.getBrowserModel().setScreenWidth(0);
const saveStub = stubUsers('updatePreferences');
+ flush();
// Setting the mode initially does not save prefs.
element.saveOnChange = true;
element.setMode(DiffViewMode.SIDE_BY_SIDE);
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 5a8c55d..9f38655b 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
@@ -87,18 +87,16 @@
});
}
- _handleSaveDiffPreferences() {
+ async _handleSaveDiffPreferences() {
this.diffPrefs = this._editableDiffPrefs;
- this.$.diffPreferences.save().then(() => {
- this.dispatchEvent(
- new CustomEvent('reload-diff-preference', {
- composed: true,
- bubbles: false,
- })
- );
-
- this.$.diffPrefsOverlay.close();
- });
+ await this.$.diffPreferences.save();
+ this.dispatchEvent(
+ new CustomEvent('reload-diff-preference', {
+ composed: true,
+ bubbles: false,
+ })
+ );
+ this.$.diffPrefsOverlay.close();
}
}
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 5c62e7a..f469799 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,7 +36,7 @@
line_wrapping: true,
};
element.diffPrefs = originalDiffPrefs;
-
+ await flush();
element.open();
await flush();
assert.isTrue(element.$.diffPreferences.$.lineWrappingInput.checked);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.ts b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.ts
index b1e15a8..4d8efed 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.ts
@@ -365,22 +365,27 @@
const type =
chunk.ab || chunk.skip ? GrDiffGroupType.BOTH : GrDiffGroupType.DELTA;
const lines = this._linesFromChunk(chunk, offsetLeft, offsetRight);
- const group = new GrDiffGroup(type, lines);
- group.keyLocation = !!chunk.keyLocation;
- group.dueToRebase = !!chunk.due_to_rebase;
- group.moveDetails = chunk.move_details;
- group.skip = chunk.skip;
- group.ignoredWhitespaceOnly = !!chunk.common;
+ const options = {
+ moveDetails: chunk.move_details,
+ dueToRebase: !!chunk.due_to_rebase,
+ ignoredWhitespaceOnly: !!chunk.common,
+ keyLocation: !!chunk.keyLocation,
+ };
if (chunk.skip) {
- group.lineRange = {
- left: {start_line: offsetLeft, end_line: offsetLeft + chunk.skip - 1},
- right: {
- start_line: offsetRight,
- end_line: offsetRight + chunk.skip - 1,
- },
- };
+ return new GrDiffGroup({
+ type,
+ skip: chunk.skip,
+ offsetLeft,
+ offsetRight,
+ ...options,
+ });
+ } else {
+ return new GrDiffGroup({
+ type,
+ lines,
+ ...options,
+ });
}
- return group;
}
_linesFromChunk(chunk: DiffContent, offsetLeft: number, offsetRight: number) {
@@ -456,7 +461,7 @@
const line = new GrDiffLine(GrDiffLineType.BOTH);
line.beforeNumber = number;
line.afterNumber = number;
- return new GrDiffGroup(GrDiffGroupType.BOTH, [line]);
+ return new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [line]});
}
/**
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 0813900..2fdb650 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
@@ -31,8 +31,9 @@
import '../gr-diff-mode-selector/gr-diff-mode-selector';
import '../gr-diff-preferences-dialog/gr-diff-preferences-dialog';
import '../gr-patch-range-select/gr-patch-range-select';
+import '../../change/gr-download-dialog/gr-download-dialog';
+import '../../shared/gr-overlay/gr-overlay';
import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-diff-view_html';
import {
KeyboardShortcutMixin,
@@ -44,7 +45,7 @@
GeneratedWebLink,
GerritNav,
} from '../../core/gr-navigation/gr-navigation';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {
computeAllPatchSets,
computeLatestPatchNum,
@@ -65,14 +66,13 @@
GrDropdownList,
} from '../../shared/gr-dropdown-list/gr-dropdown-list';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
-import {ChangeComments, GrCommentApi} from '../gr-comment-api/gr-comment-api';
+import {ChangeComments} from '../gr-comment-api/gr-comment-api';
import {GrDiffModeSelector} from '../gr-diff-mode-selector/gr-diff-mode-selector';
import {
BasePatchSetNum,
ChangeInfo,
CommitId,
ConfigInfo,
- EditInfo,
EditPatchSetNum,
FileInfo,
NumericChangeId,
@@ -83,36 +83,46 @@
RepoName,
RevisionInfo,
RevisionPatchSetNum,
+ ServerInfo,
} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
-import {ChangeViewState, CommitRange, FileRange} from '../../../types/types';
+import {
+ ChangeViewState,
+ CommitRange,
+ EditRevisionInfo,
+ FileRange,
+ ParsedChangeInfo,
+} from '../../../types/types';
import {FilesWebLinks} from '../gr-patch-range-select/gr-patch-range-select';
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
import {GrDiffCursor} from '../gr-diff-cursor/gr-diff-cursor';
import {CommentSide, DiffViewMode, Side} from '../../../constants/constants';
import {GrApplyFixDialog} from '../gr-apply-fix-dialog/gr-apply-fix-dialog';
-import {LineOfInterest} from '../gr-diff/gr-diff';
import {RevisionInfo as RevisionInfoObj} from '../../shared/revision-info/revision-info';
import {
CommentMap,
getPatchRangeForCommentUrl,
isInBaseOfPatchRange,
} from '../../../utils/comment-util';
-import {AppElementParams} from '../../gr-app-types';
+import {AppElementParams, AppElementDiffViewParam} from '../../gr-app-types';
import {EventType, OpenFixPreviewEvent} 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 {addGlobalShortcut, Key, toggleClass} from '../../../utils/dom-util';
import {CursorMoveResult} from '../../../api/core';
-import {throttleWrap} from '../../../utils/async-util';
-import {changeComments$} from '../../../services/comments/comments-model';
-import {takeUntil} from 'rxjs/operators';
-import {Subject} from 'rxjs';
-import {preferences$} from '../../../services/user/user-model';
+import {isFalse, throttleWrap, until} from '../../../utils/async-util';
+import {filter, take, switchMap} from 'rxjs/operators';
+import {combineLatest, Subscription} from 'rxjs';
import {listen} from '../../../services/shortcuts/shortcuts-service';
+import {LoadingStatus} from '../../../services/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 {resolve, DIPolymerElement} from '../../../models/dependency';
+import {BehaviorSubject} from 'rxjs';
-const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
const LOADING_BLAME = 'Loading blame...';
const LOADED_BLAME = 'Blame loaded';
@@ -131,18 +141,19 @@
export interface GrDiffView {
$: {
- commentAPI: GrCommentApi;
diffHost: GrDiffHost;
reviewed: HTMLInputElement;
dropdown: GrDropdownList;
diffPreferencesDialog: GrOverlay;
applyFixDialog: GrApplyFixDialog;
modeSelect: GrDiffModeSelector;
+ downloadOverlay: GrOverlay;
+ downloadDialog: GrDownloadDialog;
};
}
// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
+const base = KeyboardShortcutMixin(DIPolymerElement);
@customElement('gr-diff-view')
export class GrDiffView extends base {
@@ -165,7 +176,7 @@
@property({type: Object, observer: '_paramsChanged'})
params?: AppElementParams;
- @property({type: Object, notify: true, observer: '_changeViewStateChanged'})
+ @property({type: Object, notify: true})
changeViewState: Partial<ChangeViewState> = {};
@property({type: Object})
@@ -175,7 +186,7 @@
_commitRange?: CommitRange;
@property({type: Object})
- _change?: ChangeInfo;
+ _change?: ParsedChangeInfo;
@property({type: Object})
_changeComments?: ChangeComments;
@@ -220,13 +231,10 @@
_projectConfig?: ConfigInfo;
@property({type: Object})
- _userPrefs?: PreferencesInfo;
+ _serverConfig?: ServerInfo;
- @property({
- type: String,
- computed: '_getDiffViewMode(changeViewState.diffMode, _userPrefs)',
- })
- _diffMode?: string;
+ @property({type: Object})
+ _userPrefs?: PreferencesInfo;
@property({type: Boolean})
_isImageDiff?: boolean;
@@ -264,20 +272,14 @@
@property({type: Object, computed: '_getRevisionInfo(_change)'})
_revisionInfo?: RevisionInfoObj;
- @property({type: Object})
- _reviewedFiles = new Set<string>();
-
@property({type: Number})
_focusLineNum?: number;
- private getReviewedParams: {
- changeNum?: NumericChangeId;
- patchNum?: PatchSetNum;
- } = {};
-
/** Called in disconnectedCallback. */
private cleanups: (() => void)[] = [];
+ private reviewedFiles = new Set<string>();
+
override keyboardShortcuts(): ShortcutListener[] {
return [
listen(Shortcut.LEFT_PANE, _ => this.cursor.moveLeft()),
@@ -345,13 +347,26 @@
];
}
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
- private readonly commentsService = appContext.commentsService;
+ // Private but used in tests.
+ readonly routerModel = getAppContext().routerModel;
- private readonly shortcuts = appContext.shortcutsService;
+ // Private but used in tests.
+ readonly userModel = getAppContext().userModel;
+
+ // Private but used in tests.
+ readonly changeModel = getAppContext().changeModel;
+
+ // Private but used in tests.
+ readonly getBrowserModel = resolve(this, browserModelToken);
+
+ // Private but used in tests.
+ readonly getCommentsModel = resolve(this, commentsModelToken);
+
+ private readonly shortcuts = getAppContext().shortcutsService;
_throttledToggleFileReviewed?: (e: KeyboardEvent) => void;
@@ -359,30 +374,99 @@
private cursor = new GrDiffCursor();
- disconnected$ = new Subject();
+ private subscriptions: Subscription[] = [];
+
+ private connected$ = new BehaviorSubject(false);
override connectedCallback() {
super.connectedCallback();
+ this.connected$.next(true);
this._throttledToggleFileReviewed = throttleWrap(_ =>
this._handleToggleFileReviewed()
);
this._getLoggedIn().then(loggedIn => {
this._loggedIn = loggedIn;
});
- // TODO(brohlfs): This just ensures that the userService is instantiated at
- // all. We need the service to manage the model, but we are not making any
- // direct calls. Will need to find a better solution to this problem ...
- assertIsDefined(appContext.userService);
-
- changeComments$
- .pipe(takeUntil(this.disconnected$))
- .subscribe(changeComments => {
- this._changeComments = changeComments;
- });
-
- preferences$.pipe(takeUntil(this.disconnected$)).subscribe(preferences => {
- this._userPrefs = preferences;
+ this.restApiService.getConfig().then(config => {
+ this._serverConfig = config;
});
+
+ this.subscriptions.push(
+ this.getCommentsModel().changeComments$.subscribe(changeComments => {
+ this._changeComments = changeComments;
+ })
+ );
+
+ this.subscriptions.push(
+ this.userModel.preferences$.subscribe(preferences => {
+ this._userPrefs = preferences;
+ })
+ );
+ this.subscriptions.push(
+ this.userModel.diffPreferences$.subscribe(diffPreferences => {
+ this._prefs = diffPreferences;
+ })
+ );
+ this.subscriptions.push(
+ this.changeModel.change$.subscribe(change => {
+ // The diff view is tied to a specfic change number, so don't update
+ // _change to undefined.
+ if (change) this._change = change;
+ })
+ );
+
+ this.subscriptions.push(
+ this.changeModel.reviewedFiles$.subscribe(reviewedFiles => {
+ this.reviewedFiles = new Set(reviewedFiles) ?? new Set();
+ })
+ );
+
+ this.subscriptions.push(
+ this.changeModel.diffPath$.subscribe(path => (this._path = path))
+ );
+
+ this.subscriptions.push(
+ combineLatest(
+ this.changeModel.diffPath$,
+ this.changeModel.reviewedFiles$
+ ).subscribe(([path, files]) => {
+ this.$.reviewed.checked = !!path && !!files && files.includes(path);
+ })
+ );
+
+ // When user initially loads the diff view, we want to autmatically mark
+ // the file as reviewed if they have it enabled. We can't observe these
+ // properties since the method will be called anytime a property updates
+ // but we only want to call this on the initial load.
+ this.subscriptions.push(
+ this.changeModel.diffPath$
+ .pipe(
+ filter(diffPath => !!diffPath),
+ switchMap(() =>
+ combineLatest(
+ this.changeModel.currentPatchNum$,
+ this.routerModel.routerView$,
+ this.userModel.diffPreferences$,
+ this.changeModel.reviewedFiles$
+ ).pipe(
+ filter(
+ ([currentPatchNum, routerView, diffPrefs, reviewedFiles]) =>
+ !!currentPatchNum &&
+ routerView === GerritView.DIFF &&
+ !!diffPrefs &&
+ !!reviewedFiles
+ ),
+ take(1)
+ )
+ )
+ )
+ .subscribe(([currentPatchNum, _routerView, diffPrefs]) => {
+ this.setReviewedStatus(currentPatchNum!, diffPrefs);
+ })
+ );
+ this.subscriptions.push(
+ this.changeModel.diffPath$.subscribe(path => (this._path = path))
+ );
this.addEventListener('open-fix-preview', e => this._onOpenFixPreview(e));
this.cursor.replaceDiffs([this.$.diffHost]);
this._onRenderHandler = (_: Event) => {
@@ -398,16 +482,36 @@
}
override disconnectedCallback() {
- this.disconnected$.next();
this.cursor.dispose();
if (this._onRenderHandler) {
this.$.diffHost.removeEventListener('render', this._onRenderHandler);
}
for (const cleanup of this.cleanups) cleanup();
this.cleanups = [];
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions = [];
+ this.connected$.next(false);
super.disconnectedCallback();
}
+ /**
+ * Set initial review status of the file.
+ * automatically mark the file as reviewed if manual review is not set.
+ */
+
+ async setReviewedStatus(
+ currentPatchNum: PatchSetNum,
+ diffPrefs: DiffPreferencesInfo
+ ) {
+ const loggedIn = await this._getLoggedIn();
+ if (!loggedIn) return;
+ if (!diffPrefs.manual_review) {
+ this._setReviewed(true, currentPatchNum as RevisionPatchSetNum);
+ }
+ }
+
@observe('_changeComments', '_path', '_patchRange')
computeThreads(
changeComments?: ChangeComments,
@@ -440,19 +544,6 @@
});
}
- _getChangeDetail(changeNum: NumericChangeId) {
- return this.restApiService.getDiffChangeDetail(changeNum).then(change => {
- if (!change) throw new Error('Missing "change" in API response.');
- this._change = change;
- return change;
- });
- }
-
- _getChangeEdit() {
- assertIsDefined(this._changeNum, '_changeNum');
- return this.restApiService.getChangeEdit(this._changeNum);
- }
-
_getSortedFileList(files?: Files) {
if (!files) return [];
return files.sortedFileList;
@@ -502,12 +593,6 @@
});
}
- _getDiffPreferences() {
- return this.restApiService.getDiffPreferences().then(prefs => {
- this._prefs = prefs;
- });
- }
-
_getPreferences() {
return this.restApiService.getPreferences();
}
@@ -518,31 +603,19 @@
);
}
- _setReviewed(reviewed: boolean) {
+ _setReviewed(
+ reviewed: boolean,
+ patchNum: RevisionPatchSetNum | undefined = this._patchRange?.patchNum
+ ) {
if (this._editMode) return;
- this.$.reviewed.checked = reviewed;
- if (!this._patchRange?.patchNum || !this._path) return;
+ if (!patchNum || !this._path || !this._changeNum) return;
const path = this._path;
// if file is already reviewed then do not make a saveReview request
- if (this._reviewedFiles.has(path) && reviewed) return;
- if (reviewed) this._reviewedFiles.add(path);
- else this._reviewedFiles.delete(path);
- this._saveReviewedState(reviewed).catch(err => {
- if (this._reviewedFiles.has(path)) this._reviewedFiles.delete(path);
- else this._reviewedFiles.add(path);
- fireAlert(this, ERR_REVIEW_STATUS);
- throw err;
- });
- }
-
- _saveReviewedState(reviewed: boolean): Promise<Response | undefined> {
- if (!this._changeNum) return Promise.resolve(undefined);
- if (!this._patchRange?.patchNum) return Promise.resolve(undefined);
- if (!this._path) return Promise.resolve(undefined);
- return this.restApiService.saveFileReviewed(
+ if (this.reviewedFiles.has(path) && reviewed) return;
+ this.changeModel.setReviewedFilesStatus(
this._changeNum,
- this._patchRange?.patchNum,
- this._path,
+ patchNum,
+ path,
reviewed
);
}
@@ -663,11 +736,11 @@
private navigateToUnreviewedFile(direction: string) {
if (!this._path) return;
if (!this._fileList) return;
- if (!this._reviewedFiles) return;
+ if (!this.reviewedFiles) return;
// Ensure that the currently viewed file always appears in unreviewedFiles
// so we resolve the right "next" file.
const unreviewedFiles = this._fileList.filter(
- file => file === this._path || !this._reviewedFiles.has(file)
+ file => file === this._path || !this.reviewedFiles.has(file)
);
this._navToFile(this._path, unreviewedFiles, direction === 'next' ? 1 : -1);
@@ -702,8 +775,16 @@
}
_handleOpenDownloadDialog() {
- this.set('changeViewState.showDownloadDialog', true);
- this._navToChangeView();
+ this.$.downloadOverlay.open().then(() => {
+ this.$.downloadOverlay.setFocusStops(
+ this.$.downloadDialog.getFocusStops()
+ );
+ this.$.downloadDialog.focus();
+ });
+ }
+
+ _handleDownloadDialogClose() {
+ this.$.downloadOverlay.close();
}
_handleUpToChange() {
@@ -716,10 +797,13 @@
}
_handleToggleDiffMode() {
- if (this._getDiffViewMode() === DiffViewMode.SIDE_BY_SIDE) {
- this.$.modeSelect.setMode(DiffViewMode.UNIFIED);
+ if (!this._userPrefs) return;
+ if (this._userPrefs.diff_view === DiffViewMode.SIDE_BY_SIDE) {
+ this.userModel.updatePreferences({diff_view: DiffViewMode.UNIFIED});
} else {
- this.$.modeSelect.setMode(DiffViewMode.SIDE_BY_SIDE);
+ this.userModel.updatePreferences({
+ diff_view: DiffViewMode.SIDE_BY_SIDE,
+ });
}
}
@@ -856,28 +940,6 @@
return {path: fileList[idx]};
}
- _getReviewedFiles(changeNum?: NumericChangeId, patchNum?: PatchSetNum) {
- if (!changeNum || !patchNum) return;
- if (
- this.getReviewedParams.changeNum === changeNum &&
- this.getReviewedParams.patchNum === patchNum
- ) {
- return;
- }
- this.getReviewedParams = {
- changeNum,
- patchNum,
- };
- this.restApiService.getReviewedFiles(changeNum, patchNum).then(files => {
- this._reviewedFiles = new Set(files);
- });
- }
-
- _getReviewedStatus(path: string) {
- if (this._editMode) return false;
- return this._reviewedFiles.has(path);
- }
-
_initLineOfInterestAndCursor(leftSide: boolean) {
this.$.diffHost.lineOfInterest = this._getLineOfInterest(leftSide);
this._initCursor(leftSide);
@@ -975,7 +1037,7 @@
GerritNav.navigateToChange(this._change);
return;
}
- this._path = comment.path;
+ this.changeModel.updatePath(comment.path);
const latestPatchNum = computeLatestPatchNum(this._allPatchSets);
if (!latestPatchNum) throw new Error('Missing _allPatchSets');
@@ -985,7 +1047,7 @@
this._focusLineNum = comment.line;
} else {
if (this.params.path) {
- this._path = this.params.path;
+ this.changeModel.updatePath(this.params.path);
}
if (this.params.patchNum) {
this._patchRange = {
@@ -1017,21 +1079,41 @@
);
}
+ private isSameDiffLoaded(value: AppElementDiffViewParam) {
+ return (
+ this._patchRange?.basePatchNum === value.basePatchNum &&
+ this._patchRange?.patchNum === value.patchNum &&
+ this._path === value.path
+ );
+ }
+
_paramsChanged(value: AppElementParams) {
if (value.view !== GerritView.DIFF) {
return;
}
+ // The diff view is kept in the background once created. If the user
+ // scrolls in the change page, the scrolling is reflected in the diff view
+ // as well, which means the diff is scrolled to a random position based
+ // on how much the change view was scrolled.
+ // Hence, reset the scroll position here.
+ document.documentElement.scrollTop = 0;
+
// Everything in the diff view is tied to the change. It seems better to
// force the re-creation of the diff view when the change number changes.
const changeChanged = this._changeNum !== value.changeNum;
if (this._changeNum !== undefined && changeChanged) {
fireEvent(this, EventType.RECREATE_DIFF_VIEW);
return;
+ } else if (this._changeNum !== undefined && this.isSameDiffLoaded(value)) {
+ // changeNum has not changed, so check if there are changes in patchRange
+ // path. If no changes then we can simply render the view as is.
+ this.reporting.reportInteraction('diff-view-re-rendered');
+ return;
}
this._files = {sortedFileList: [], changeFilesByPath: {}};
- this._path = undefined;
+ this.changeModel.updatePath(undefined);
this._patchRange = undefined;
this._commitRange = undefined;
this._focusLineNum = undefined;
@@ -1055,32 +1137,24 @@
}
const promises: Promise<unknown>[] = [];
-
- promises.push(this._getDiffPreferences());
-
- if (!this._change) promises.push(this._getChangeDetail(this._changeNum));
-
- if (!this._changeComments) this._loadComments(value.patchNum);
-
- promises.push(this._getChangeEdit());
+ if (!this._change) {
+ promises.push(
+ until(
+ this.changeModel.changeLoadingStatus$,
+ status => status === LoadingStatus.LOADED
+ )
+ );
+ }
+ promises.push(this.waitUntilCommentsLoaded());
this.$.diffHost.cancel();
this.$.diffHost.clearDiffContent();
this._loading = true;
return Promise.all(promises)
- .then(r => {
+ .then(() => {
this._loading = false;
this._initPatchRange();
this._initCommitRange();
-
- const edit = r[4] as EditInfo | undefined;
- if (edit) {
- this.set(`_change.revisions.${edit.commit.commit}`, {
- _number: EditPatchSetNum,
- basePatchNum: edit.base_patch_set_number,
- commit: edit.commit,
- });
- }
return this.$.diffHost.reload(true);
})
.then(() => {
@@ -1126,52 +1200,9 @@
});
}
- _changeViewStateChanged(changeViewState: Partial<ChangeViewState>) {
- if (changeViewState.diffMode === null) {
- // If screen size is small, always default to unified view.
- this.restApiService.getPreferences().then(prefs => {
- if (prefs) {
- this.set('changeViewState.diffMode', prefs.default_diff_view);
- }
- });
- }
- }
-
- @observe('_path', '_prefs', '_reviewedFiles', '_patchRange')
- _setReviewedObserver(
- path?: string,
- prefs?: DiffPreferencesInfo,
- reviewedFiles?: Set<string>,
- patchRange?: PatchRange
- ) {
- if (prefs === undefined) return;
- if (path === undefined) return;
- if (reviewedFiles === undefined) return;
- if (patchRange === undefined) return;
- if (prefs.manual_review) {
- // Checkbox state needs to be set explicitly only when manual_review
- // is specified.
- this.$.reviewed.checked = this._getReviewedStatus(path);
- } else {
- this._setReviewed(true);
- }
- }
-
- @observe('_loggedIn', '_changeNum', '_patchRange')
- getReviewedFiles(
- _loggedIn?: boolean,
- _changeNum?: NumericChangeId,
- patchRange?: PatchRange
- ) {
- if (_loggedIn === undefined) return;
- if (_changeNum === undefined) return;
- if (patchRange === undefined) return;
-
- if (!_loggedIn) {
- return;
- }
-
- this._getReviewedFiles(this._changeNum, patchRange.patchNum);
+ private async waitUntilCommentsLoaded() {
+ await until(this.connected$, c => c);
+ await until(this.getCommentsModel().commentsLoading$, isFalse);
}
/**
@@ -1189,14 +1220,17 @@
this.cursor.initialLineNumber = this._focusLineNum;
}
- _getLineOfInterest(leftSide: boolean): LineOfInterest | undefined {
+ _getLineOfInterest(leftSide: boolean): DisplayLine | undefined {
// If there is a line number specified, pass it along to the diff so that
// it will not get collapsed.
if (!this._focusLineNum) {
return undefined;
}
- return {number: this._focusLineNum, leftSide};
+ return {
+ lineNum: this._focusLineNum,
+ side: leftSide ? Side.LEFT : Side.RIGHT,
+ };
}
_pathChanged(path: string) {
@@ -1209,7 +1243,11 @@
this.set('changeViewState.selectedFileIndex', this._fileList.indexOf(path));
}
- _getDiffUrl(change?: ChangeInfo, patchRange?: PatchRange, path?: string) {
+ _getDiffUrl(
+ change?: ChangeInfo | ParsedChangeInfo,
+ patchRange?: PatchRange,
+ path?: string
+ ) {
if (!change || !patchRange || !path) return '';
return GerritNav.getUrlForDiff(
change,
@@ -1226,7 +1264,7 @@
*/
_getChangeUrlRange(
patchRange?: PatchRange,
- revisions?: {[revisionId: string]: RevisionInfo}
+ revisions?: {[revisionId: string]: RevisionInfo | EditRevisionInfo}
) {
let patchNum = undefined;
let basePatchNum = undefined;
@@ -1248,29 +1286,31 @@
}
_getChangePath(
- change?: ChangeInfo,
+ change?: ChangeInfo | ParsedChangeInfo,
patchRange?: PatchRange,
- revisions?: {[revisionId: string]: RevisionInfo}
+ revisions?: {[revisionId: string]: RevisionInfo | EditRevisionInfo}
) {
if (!change) return '';
if (!patchRange) return '';
const range = this._getChangeUrlRange(patchRange, revisions);
- return GerritNav.getUrlForChange(
- change,
- range.patchNum,
- range.basePatchNum
- );
+ return GerritNav.getUrlForChange(change, {
+ patchNum: range.patchNum,
+ basePatchNum: range.basePatchNum,
+ });
}
_navigateToChange(
- change?: ChangeInfo,
+ change?: ChangeInfo | ParsedChangeInfo,
patchRange?: PatchRange,
- revisions?: {[revisionId: string]: RevisionInfo}
+ revisions?: {[revisionId: string]: RevisionInfo | EditRevisionInfo}
) {
if (!change) return;
const range = this._getChangeUrlRange(patchRange, revisions);
- GerritNav.navigateToChange(change, range.patchNum, range.basePatchNum);
+ GerritNav.navigateToChange(change, {
+ patchNum: range.patchNum,
+ basePatchNum: range.basePatchNum,
+ });
}
_computeChangePath(
@@ -1351,29 +1391,6 @@
this.$.diffPreferencesDialog.open();
}
- /**
- * _getDiffViewMode: Get the diff view (side-by-side or unified) based on
- * the current state.
- *
- * The expected behavior is to use the mode specified in the user's
- * preferences unless they have manually chosen the alternative view or they
- * are on a mobile device. If the user navigates up to the change view, it
- * should clear this choice and revert to the preference the next time a
- * diff is viewed.
- *
- * Use side-by-side if the user is not logged in.
- */
- _getDiffViewMode() {
- if (this.changeViewState.diffMode) {
- return this.changeViewState.diffMode;
- } else if (this._userPrefs) {
- this.set('changeViewState.diffMode', this._userPrefs.default_diff_view);
- return this._userPrefs.default_diff_view;
- } else {
- return 'SIDE_BY_SIDE';
- }
- }
-
_computeModeSelectHideClass(diff?: DiffInfo) {
return !diff || diff.binary ? 'hide' : '';
}
@@ -1484,11 +1501,6 @@
return url;
}
- _loadComments(patchSet?: PatchSetNum) {
- assertIsDefined(this._changeNum, '_changeNum');
- return this.commentsService.loadAll(this._changeNum, patchSet);
- }
-
@observe(
'_changeComments',
'_files.changeFilesByPath',
@@ -1744,7 +1756,7 @@
}
_handleReloadingDiffPreference() {
- this._getDiffPreferences();
+ this.userModel.getDiffPreferences();
}
_computeCanEdit(
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
index 308c353..ef38440 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
@@ -277,7 +277,6 @@
<gr-patch-range-select
id="rangeSelect"
change-num="[[_changeNum]]"
- change-comments="[[_changeComments]]"
patch-num="[[_patchRange.patchNum]]"
base-patch-num="[[_patchRange.basePatchNum]]"
files-weblinks="[[_filesWeblinks]]"
@@ -339,7 +338,6 @@
<gr-diff-mode-selector
id="modeSelect"
save-on-change="[[_loggedIn]]"
- mode="{{changeViewState.diffMode}}"
show-tooltip-below=""
></gr-diff-mode-selector>
</div>
@@ -409,7 +407,6 @@
path="[[_path]]"
prefs="[[_prefs]]"
project-name="[[_change.project]]"
- view-mode="[[_diffMode]]"
is-blame-loaded="{{_isBlameLoaded}}"
on-comment-anchor-tap="_onLineSelected"
on-line-selected="_onLineSelected"
@@ -428,5 +425,13 @@
on-reload-diff-preference="_handleReloadingDiffPreference"
>
</gr-diff-preferences-dialog>
- <gr-comment-api id="commentAPI"></gr-comment-api>
+ <gr-overlay id="downloadOverlay">
+ <gr-download-dialog
+ id="downloadDialog"
+ change="[[_change]]"
+ patch-num="[[_patchRange.patchNum]]"
+ config="[[_serverConfig.download]]"
+ on-close="_handleDownloadDialogClose"
+ ></gr-download-dialog>
+ </gr-overlay>
`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
index e4a8aa4..77bbded 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
@@ -18,23 +18,22 @@
import '../../../test/common-test-setup-karma.js';
import './gr-diff-view.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {ChangeStatus} from '../../../constants/constants.js';
-import {stubRestApi} from '../../../test/test-utils.js';
+import {ChangeStatus, DiffViewMode, createDefaultDiffPrefs} from '../../../constants/constants.js';
+import {stubRestApi, stubUsers, waitUntil, stubChange} from '../../../test/test-utils.js';
import {ChangeComments} from '../gr-comment-api/gr-comment-api.js';
import {GerritView} from '../../../services/router/router-model.js';
import {
createChange,
createRevisions,
createComment,
+ TEST_NUMERIC_CHANGE_ID,
} from '../../../test/test-data-generators.js';
import {EditPatchSetNum} from '../../../types/common.js';
import {CursorMoveResult} from '../../../api/core.js';
-import {EventType} from '../../../types/events.js';
+import {Side} from '../../../api/diff.js';
const basicFixture = fixtureFromElement('gr-diff-view');
-const blankFixture = fixtureFromElement('div');
-
suite('gr-diff-view tests', () => {
suite('basic tests', () => {
let element;
@@ -54,14 +53,10 @@
};
}
- let getDiffChangeDetailStub;
setup(async () => {
- clock = sinon.useFakeTimers();
stubRestApi('getConfig').returns(Promise.resolve({change: {}}));
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
stubRestApi('getProjectConfig').returns(Promise.resolve({}));
- getDiffChangeDetailStub = stubRestApi('getDiffChangeDetail').returns(
- Promise.resolve({}));
stubRestApi('getChangeFiles').returns(Promise.resolve({}));
stubRestApi('saveFileReviewed').returns(Promise.resolve());
diffCommentsStub = stubRestApi('getDiffComments');
@@ -95,10 +90,19 @@
},
]});
await flush();
+
+ element.getCommentsModel().setState({
+ comments: {},
+ robotComments: {},
+ drafts: {},
+ portedComments: {},
+ portedDrafts: {},
+ discardedDrafts: [],
+ });
});
teardown(() => {
- clock.restore();
+ clock && clock.restore();
sinon.restore();
});
@@ -133,29 +137,37 @@
sinon.stub(element.reporting, 'diffViewDisplayed');
sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
sinon.spy(element, '_paramsChanged');
- sinon.stub(element, '_getChangeDetail').returns(Promise.resolve({
- ...createChange(),
- revisions: createRevisions(11),
- }));
+ element.changeModel.setState({
+ change: {
+ ...createChange(),
+ revisions: createRevisions(11),
+ }});
});
test('comment url resolves to comment.patch_set vs latest', () => {
- diffCommentsStub.returns(Promise.resolve({
- '/COMMIT_MSG': [
- {
- ...createComment(),
- id: 'c1',
- line: 10,
- patch_set: 2,
- path: '/COMMIT_MSG',
- }, {
- ...createComment(),
- id: 'c3',
- line: 10,
- patch_set: 'PARENT',
- path: '/COMMIT_MSG',
- },
- ]}));
+ element.getCommentsModel().setState({
+ comments: {
+ '/COMMIT_MSG': [
+ {
+ ...createComment(),
+ id: 'c1',
+ line: 10,
+ patch_set: 2,
+ path: '/COMMIT_MSG',
+ }, {
+ ...createComment(),
+ id: 'c3',
+ line: 10,
+ patch_set: 'PARENT',
+ path: '/COMMIT_MSG',
+ },
+ ]},
+ robotComments: {},
+ drafts: {},
+ portedComments: {},
+ portedDrafts: {},
+ discardedDrafts: [],
+ });
element.params = {
view: GerritNav.View.DIFF,
changeNum: '42',
@@ -207,31 +219,39 @@
test('unchanged diff X vs latest from comment links navigates to base vs X'
, () => {
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
- diffCommentsStub.returns(Promise.resolve({
- '/COMMIT_MSG': [
- {
- ...createComment(),
- id: 'c1',
- line: 10,
- patch_set: 2,
- path: '/COMMIT_MSG',
- }, {
- ...createComment(),
- id: 'c3',
- line: 10,
- patch_set: 'PARENT',
- path: '/COMMIT_MSG',
- },
- ]}));
+ element.getCommentsModel().setState({
+ comments: {
+ '/COMMIT_MSG': [
+ {
+ ...createComment(),
+ id: 'c1',
+ line: 10,
+ patch_set: 2,
+ path: '/COMMIT_MSG',
+ }, {
+ ...createComment(),
+ id: 'c3',
+ line: 10,
+ patch_set: 'PARENT',
+ path: '/COMMIT_MSG',
+ },
+ ]},
+ robotComments: {},
+ drafts: {},
+ portedComments: {},
+ portedDrafts: {},
+ discardedDrafts: [],
+ });
sinon.stub(element.reporting, 'diffViewDisplayed');
sinon.stub(element, '_loadBlame');
sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
sinon.stub(element, '_isFileUnchanged').returns(true);
sinon.spy(element, '_paramsChanged');
- sinon.stub(element, '_getChangeDetail').returns(Promise.resolve({
- ...createChange(),
- revisions: createRevisions(11),
- }));
+ element.changeModel.setState({
+ change: {
+ ...createChange(),
+ revisions: createRevisions(11),
+ }});
element.params = {
view: GerritNav.View.DIFF,
changeNum: '42',
@@ -252,31 +272,39 @@
test('unchanged diff Base vs latest from comment does not navigate'
, () => {
const diffNavStub = sinon.stub(GerritNav, 'navigateToDiff');
- diffCommentsStub.returns(Promise.resolve({
- '/COMMIT_MSG': [
- {
- ...createComment(),
- id: 'c1',
- line: 10,
- patch_set: 2,
- path: '/COMMIT_MSG',
- }, {
- ...createComment(),
- id: 'c3',
- line: 10,
- patch_set: 'PARENT',
- path: '/COMMIT_MSG',
- },
- ]}));
+ element.getCommentsModel().setState({
+ comments: {
+ '/COMMIT_MSG': [
+ {
+ ...createComment(),
+ id: 'c1',
+ line: 10,
+ patch_set: 2,
+ path: '/COMMIT_MSG',
+ }, {
+ ...createComment(),
+ id: 'c3',
+ line: 10,
+ patch_set: 'PARENT',
+ path: '/COMMIT_MSG',
+ },
+ ]},
+ robotComments: {},
+ drafts: {},
+ portedComments: {},
+ portedDrafts: {},
+ discardedDrafts: [],
+ });
sinon.stub(element.reporting, 'diffViewDisplayed');
sinon.stub(element, '_loadBlame');
sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
sinon.stub(element, '_isFileUnchanged').returns(true);
sinon.spy(element, '_paramsChanged');
- sinon.stub(element, '_getChangeDetail').returns(Promise.resolve({
- ...createChange(),
- revisions: createRevisions(11),
- }));
+ element.changeModel.setState({
+ change: {
+ ...createChange(),
+ revisions: createRevisions(11),
+ }});
element.params = {
view: GerritNav.View.DIFF,
changeNum: '42',
@@ -324,87 +352,41 @@
assert.equal(element._isFileUnchanged(diff), true);
});
- test('change detail is not rerequested if changeNum doesnt change',
- async () => {
- const dispatchEventStub = sinon.stub(element, 'dispatchEvent');
- assert.isFalse(getDiffChangeDetailStub.called);
- sinon.stub(element.reporting, 'diffViewDisplayed');
- sinon.stub(element, '_loadBlame');
- sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
- sinon.spy(element, '_paramsChanged');
- element._change = undefined;
- getDiffChangeDetailStub.returns(
- Promise.resolve({
- ...createChange(),
- revisions: createRevisions(11),
- }));
- element._patchRange = {
- patchNum: 2,
- basePatchNum: 1,
- };
- sinon.stub(element, '_isFileUnchanged').returns(false);
-
- element.params = {
- view: GerritNav.View.DIFF,
- changeNum: '42',
- project: 'p',
- commentId: 'c1',
- commentLink: true,
- };
- await element._paramsChanged.returnValues[0];
-
- assert.equal(getDiffChangeDetailStub.callCount, 1);
- element.params = {
- view: GerritNav.View.DIFF,
- changeNum: '42',
- project: 'p',
- commentId: 'c1',
- commentLink: true,
- };
- await element._paramsChanged.returnValues[0];
-
- assert.equal(getDiffChangeDetailStub.callCount, 1);
- element.params = {
- view: GerritNav.View.DIFF,
- changeNum: '43',
- project: 'p',
- commentId: 'c1',
- commentLink: true,
- };
- await element._paramsChanged.returnValues[0];
-
- // change page is recreated now
- assert.equal(dispatchEventStub.lastCall.args[0].type,
- EventType.RECREATE_DIFF_VIEW);
- });
-
test('diff toast to go to latest is shown and not base', async () => {
- diffCommentsStub.returns(Promise.resolve({
- '/COMMIT_MSG': [
- {
- ...createComment(),
- id: 'c1',
- line: 10,
- patch_set: 2,
- path: '/COMMIT_MSG',
- }, {
- ...createComment(),
- id: 'c3',
- line: 10,
- patch_set: 'PARENT',
- path: '/COMMIT_MSG',
- },
- ]}));
+ element.getCommentsModel().setState({
+ comments: {
+ '/COMMIT_MSG': [
+ {
+ ...createComment(),
+ id: 'c1',
+ line: 10,
+ patch_set: 2,
+ path: '/COMMIT_MSG',
+ }, {
+ ...createComment(),
+ id: 'c3',
+ line: 10,
+ patch_set: 'PARENT',
+ path: '/COMMIT_MSG',
+ },
+ ]},
+ robotComments: {},
+ drafts: {},
+ portedComments: {},
+ portedDrafts: {},
+ discardedDrafts: [],
+ });
+
sinon.stub(element.reporting, 'diffViewDisplayed');
sinon.stub(element, '_loadBlame');
sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
sinon.spy(element, '_paramsChanged');
element._change = undefined;
- getDiffChangeDetailStub.returns(
- Promise.resolve({
- ...createChange(),
- revisions: createRevisions(11),
- }));
+ element.changeModel.setState({
+ change: {
+ ...createChange(),
+ revisions: createRevisions(11),
+ }});
element._patchRange = {
patchNum: 2,
basePatchNum: 1,
@@ -431,7 +413,9 @@
});
test('keyboard shortcuts', () => {
+ clock = sinon.useFakeTimers();
element._changeNum = '42';
+ element.getBrowserModel().setScreenWidth(0);
element._patchRange = {
basePatchNum: PARENT,
patchNum: 10,
@@ -510,11 +494,11 @@
'_computeContainerClass');
MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
assert(computeContainerClassStub.lastCall.calledWithExactly(
- false, 'SIDE_BY_SIDE', true));
+ false, DiffViewMode.SIDE_BY_SIDE, true));
MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'Escape');
assert(computeContainerClassStub.lastCall.calledWithExactly(
- false, 'SIDE_BY_SIDE', false));
+ false, DiffViewMode.SIDE_BY_SIDE, false));
// Note that stubbing _setReviewed means that the value of the
// `element.$.reviewed` checkbox is not flipped.
@@ -539,6 +523,7 @@
MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
assert.isTrue(element._handleToggleFileReviewed.calledTwice);
assert.isTrue(element._setReviewed.calledTwice);
+ clock.restore();
});
test('moveToNextCommentThread navigates to next file', () => {
@@ -727,8 +712,8 @@
MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
await flush();
assert.isTrue(element.changeViewState.showReplyDialog);
- assert(changeNavStub.lastCall.calledWithExactly(element._change, 10,
- 5), 'Should navigate to /c/42/5..10');
+ assert(changeNavStub.lastCall.calledWithExactly(element._change, {
+ patchNum: 10, basePatchNum: 5}), 'Should navigate to /c/42/5..10');
assert.isFalse(loggedInErrorSpy.called);
});
@@ -753,8 +738,8 @@
MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
await flush();
assert.isTrue(element.changeViewState.showReplyDialog);
- assert(changeNavStub.lastCall.calledWithExactly(element._change, 1,
- PARENT), 'Should navigate to /c/42/1');
+ assert(changeNavStub.lastCall.calledWithExactly(element._change, {
+ patchNum: 1, basePatchNum: PARENT}), 'Should navigate to /c/42/1');
assert.isFalse(loggedInErrorSpy.called);
});
@@ -779,8 +764,8 @@
const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
- assert(changeNavStub.lastCall.calledWithExactly(element._change, 10,
- 5), 'Should navigate to /c/42/5..10');
+ assert(changeNavStub.lastCall.calledWithExactly(element._change,
+ {patchNum: 10, basePatchNum: 5}), 'Should navigate to /c/42/5..10');
MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
assert.isTrue(element._loading);
@@ -809,13 +794,14 @@
MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
assert.isTrue(element._loading);
- assert(changeNavStub.lastCall.calledWithExactly(element._change, 10,
- 5),
+ assert(changeNavStub.lastCall.calledWithExactly(element._change,
+ {patchNum: 10, basePatchNum: 5}),
'Should navigate to /c/42/5..10');
- assert.isUndefined(element.changeViewState.showDownloadDialog);
+ const downloadOverlayStub = sinon.stub(element.$.downloadOverlay, 'open')
+ .returns(Promise.resolve());
MockInteractions.pressAndReleaseKeyOn(element, 68, null, 'd');
- assert.isTrue(element.changeViewState.showDownloadDialog);
+ assert.isTrue(downloadOverlayStub.called);
});
test('keyboard shortcuts with old patch number', () => {
@@ -839,8 +825,8 @@
const changeNavStub = sinon.stub(GerritNav, 'navigateToChange');
MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
- assert(changeNavStub.lastCall.calledWithExactly(element._change, 1,
- PARENT), 'Should navigate to /c/42/1');
+ assert(changeNavStub.lastCall.calledWithExactly(element._change,
+ {patchNum: 1, basePatchNum: PARENT}), 'Should navigate to /c/42/1');
MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']');
assert(diffNavStub.lastCall.calledWithExactly(element._change,
@@ -865,8 +851,8 @@
changeNavStub.reset();
MockInteractions.pressAndReleaseKeyOn(element, 219, null, '[');
- assert(changeNavStub.lastCall.calledWithExactly(element._change, 1,
- PARENT), 'Should navigate to /c/42/1');
+ assert(changeNavStub.lastCall.calledWithExactly(element._change,
+ {patchNum: 1, basePatchNum: PARENT}), 'Should navigate to /c/42/1');
assert.isTrue(changeNavStub.calledOnce);
});
@@ -991,7 +977,9 @@
});
suite('diff prefs hidden', () => {
- test('whenlogged out', () => {
+ test('when no prefs or logged out', () => {
+ element._prefs = undefined;
+ element.disableDiffPrefs = false;
element._loggedIn = false;
flush();
assert.isTrue(element.$.diffPrefsContainer.hidden);
@@ -1035,7 +1023,8 @@
sinon.stub(
GerritNav
, 'getUrlForChange')
- .callsFake((c, pn, bpn) => `${c._number}-${pn}-${bpn}`);
+ .callsFake((c, ops) =>
+ `${c._number}-${ops.patchNum}-${ops.basePatchNum}`);
});
test('_formattedFiles', () => {
@@ -1199,77 +1188,128 @@
element._path, 1, 'PARENT'));
});
- test('_prefs.manual_review is respected', () => {
- const saveReviewedStub = sinon.stub(element, '_saveReviewedState')
+ test('_prefs.manual_review true means set reviewed is not ' +
+ 'automatically called', async () => {
+ const setReviewedFileStatusStub = stubChange('setReviewedFilesStatus')
.callsFake(() => Promise.resolve());
- const getReviewedStub = sinon.stub(element, '_getReviewedStatus')
- .returns(false);
+
+ const setReviewedStatusStub = sinon.spy(element, 'setReviewedStatus');
sinon.stub(element.$.diffHost, 'reload');
- element._loggedIn = true;
- element._prefs = {manual_review: true};
- element.params = {
- view: GerritNav.View.DIFF,
- changeNum: '42',
- patchNum: 2,
- basePatchNum: 1,
- path: '/COMMIT_MSG',
+ sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
+ const diffPreferences = {
+ ...createDefaultDiffPrefs(),
+ manual_review: true,
};
+ element.userModel.setDiffPreferences(diffPreferences);
+ element.changeModel.setState({
+ change: createChange(),
+ diffPath: '/COMMIT_MSG',
+ reviewedFiles: [],
+ });
+
+ element.routerModel.setState({
+ changeNum: TEST_NUMERIC_CHANGE_ID, view: GerritView.DIFF, patchNum: 2}
+ );
element._patchRange = {
patchNum: 2,
basePatchNum: 1,
};
- flush();
- assert.isFalse(saveReviewedStub.called);
- assert.isTrue(getReviewedStub.called);
+ await waitUntil(() => setReviewedStatusStub.called);
- const oldCount = getReviewedStub.callCount;
+ assert.isFalse(setReviewedFileStatusStub.called);
- element._prefs = {};
- element._path = 'abcd';
- flush();
+ // if prefs are updated then the reviewed status should not be set again
+ element.userModel.setDiffPreferences(createDefaultDiffPrefs());
- assert.isTrue(saveReviewedStub.called);
- assert.equal(getReviewedStub.callCount, oldCount);
+ await flush();
+ assert.isFalse(setReviewedFileStatusStub.called);
});
- test('file review status', () => {
- const saveReviewedStub = sinon.stub(element, '_saveReviewedState')
+ test('_prefs.manual_review false means set reviewed is called',
+ async () => {
+ const setReviewedFileStatusStub = stubChange('setReviewedFilesStatus')
+ .callsFake(() => Promise.resolve());
+
+ sinon.stub(element.$.diffHost, 'reload');
+ sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
+ const diffPreferences = {
+ ...createDefaultDiffPrefs(),
+ manual_review: false,
+ };
+ element.userModel.setDiffPreferences(diffPreferences);
+ element.changeModel.setState({
+ change: createChange(),
+ diffPath: '/COMMIT_MSG',
+ reviewedFiles: [],
+ });
+
+ element.routerModel.setState({
+ changeNum: TEST_NUMERIC_CHANGE_ID, view: GerritView.DIFF,
+ patchNum: 22}
+ );
+ element._patchRange = {
+ patchNum: 2,
+ basePatchNum: 1,
+ };
+
+ await waitUntil(() => setReviewedFileStatusStub.called);
+
+ assert.isTrue(setReviewedFileStatusStub.called);
+ });
+
+ test('file review status', async () => {
+ element.changeModel.setState({
+ change: createChange(),
+ diffPath: '/COMMIT_MSG',
+ reviewedFiles: [],
+ });
+ sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
+ const saveReviewedStub = stubChange('setReviewedFilesStatus')
.callsFake(() => Promise.resolve());
sinon.stub(element.$.diffHost, 'reload');
- element._loggedIn = true;
- element.params = {
- view: GerritNav.View.DIFF,
- changeNum: '42',
- patchNum: 2,
- basePatchNum: 1,
- path: '/COMMIT_MSG',
- };
+ element.userModel.setDiffPreferences(createDefaultDiffPrefs());
+
+ element.routerModel.setState({
+ changeNum: TEST_NUMERIC_CHANGE_ID, view: GerritView.DIFF, patchNum: 2}
+ );
+
element._patchRange = {
patchNum: 2,
basePatchNum: 1,
};
- element._path = 'abcd';
- element._prefs = {};
- flush();
- const commitMsg = element.root.querySelector(
+ await waitUntil(() => saveReviewedStub.called);
+
+ element.changeModel.updateStateFileReviewed('/COMMIT_MSG', true);
+ await flush();
+
+ const reviewedStatusCheckBox = element.root.querySelector(
'input[type="checkbox"]');
- assert.isTrue(commitMsg.checked);
- MockInteractions.tap(commitMsg);
- assert.isFalse(commitMsg.checked);
- assert.isTrue(saveReviewedStub.lastCall.calledWithExactly(false));
+ assert.isTrue(reviewedStatusCheckBox.checked);
+ assert.deepEqual(saveReviewedStub.lastCall.args,
+ ['42', 2, '/COMMIT_MSG', true]);
- MockInteractions.tap(commitMsg);
- assert.isTrue(commitMsg.checked);
- assert.isTrue(saveReviewedStub.lastCall.calledWithExactly(true));
+ MockInteractions.tap(reviewedStatusCheckBox);
+ assert.isFalse(reviewedStatusCheckBox.checked);
+ assert.deepEqual(saveReviewedStub.lastCall.args,
+ ['42', 2, '/COMMIT_MSG', false]);
+
+ element.changeModel.updateStateFileReviewed('/COMMIT_MSG', false);
+ await flush();
+
+ MockInteractions.tap(reviewedStatusCheckBox);
+ assert.isTrue(reviewedStatusCheckBox.checked);
+ assert.deepEqual(saveReviewedStub.lastCall.args,
+ ['42', 2, '/COMMIT_MSG', true]);
+
const callCount = saveReviewedStub.callCount;
element.set('params.view', GerritNav.View.CHANGE);
- flush();
+ await flush();
// saveReviewedState observer observes params, but should not fire when
// view !== GerritNav.View.DIFF.
@@ -1277,7 +1317,7 @@
});
test('file review status with edit loaded', () => {
- const saveReviewedStub = sinon.stub(element, '_saveReviewedState');
+ const saveReviewedStub = stubChange('setReviewedFilesStatus');
element._patchRange = {patchNum: EditPatchSetNum};
flush();
@@ -1308,47 +1348,23 @@
test('diff mode selector correctly toggles the diff', () => {
const select = element.$.modeSelect;
const diffDisplay = element.$.diffHost;
- element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'};
+ element._userPrefs = {diff_view: DiffViewMode.SIDE_BY_SIDE};
+ element.getBrowserModel().setScreenWidth(0);
+ const userStub = stubUsers('updatePreferences');
+
+ flush();
// The mode selected in the view state reflects the selected option.
- assert.equal(element._getDiffViewMode(), select.mode);
+ // assert.equal(element._userPrefs.diff_view, select.mode);
// The mode selected in the view state reflects the view rednered in the
// diff.
assert.equal(select.mode, diffDisplay.viewMode);
// We will simulate a user change of the selected mode.
- const newMode = 'UNIFIED_DIFF';
-
- // Set the mode, and simulate the change event.
- element.set('changeViewState.diffMode', newMode);
-
- // Make sure the handler was called and the state is still coherent.
- assert.equal(element._getDiffViewMode(), newMode);
- assert.equal(element._getDiffViewMode(), select.mode);
- assert.equal(element._getDiffViewMode(), diffDisplay.viewMode);
- });
-
- test('diff mode selector initializes from preferences', () => {
- let resolvePrefs;
- const prefsPromise = new Promise(resolve => {
- resolvePrefs = resolve;
- });
- stubRestApi('getPreferences')
- .callsFake(() => prefsPromise);
-
- // Attach a new gr-diff-view so we can intercept the preferences fetch.
- const view = document.createElement('gr-diff-view');
- blankFixture.instantiate().appendChild(view);
- flush();
-
- // At this point the diff mode doesn't yet have the user's preference.
- assert.equal(view._getDiffViewMode(), 'SIDE_BY_SIDE');
-
- // Receive the overriding preference.
- resolvePrefs({default_diff_view: 'UNIFIED'});
- flush();
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
+ element._handleToggleDiffMode();
+ assert.isTrue(userStub.calledWithExactly({
+ diff_view: DiffViewMode.UNIFIED}));
});
test('diff mode selector should be hidden for binary', async () => {
@@ -1385,8 +1401,6 @@
sinon.stub(element.$.diffHost, 'reload');
sinon.stub(element, '_initCursor');
element._change = change;
- sinon.stub(element, '_getChangeDetail').returns(Promise.resolve(
- change));
});
test('uses the patchNum and basePatchNum ', async () => {
@@ -1457,12 +1471,12 @@
element._focusLineNum = 12;
let result = element._getLineOfInterest(false);
- assert.equal(result.number, 12);
- assert.isNotOk(result.leftSide);
+ assert.equal(result.lineNum, 12);
+ assert.equal(result.side, Side.RIGHT);
result = element._getLineOfInterest(true);
- assert.equal(result.number, 12);
- assert.isOk(result.leftSide);
+ assert.equal(result.lineNum, 12);
+ assert.equal(result.side, Side.LEFT);
});
test('_onLineSelected', () => {
@@ -1509,32 +1523,22 @@
assert.isTrue(getUrlStub.lastCall.args[6]);
});
- test('_getDiffViewMode', () => {
- // No user prefs or change view state set.
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
-
- // User prefs but no change view state set.
- element.changeViewState.diffMode = undefined;
- element._userPrefs = {default_diff_view: 'UNIFIED_DIFF'};
- assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');
-
- // User prefs and change view state set.
- element.changeViewState = {diffMode: 'SIDE_BY_SIDE'};
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
- });
-
test('_handleToggleDiffMode', () => {
+ const userStub = stubUsers('updatePreferences');
const e = new CustomEvent('keydown', {
detail: {keyboardEvent: new KeyboardEvent('keydown'), key: 'x'},
});
- // Initial state.
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
+ element._userPrefs = {diff_view: DiffViewMode.SIDE_BY_SIDE};
element._handleToggleDiffMode(e);
- assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');
+ assert.deepEqual(userStub.lastCall.args[0], {
+ diff_view: DiffViewMode.UNIFIED});
+
+ element._userPrefs = {diff_view: DiffViewMode.UNIFIED};
element._handleToggleDiffMode(e);
- assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
+ assert.deepEqual(userStub.lastCall.args[0], {
+ diff_view: DiffViewMode.SIDE_BY_SIDE});
});
suite('_initPatchRange', () => {
@@ -1802,7 +1806,7 @@
isAtEndStub.returns(true);
element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
- element._reviewedFiles = new Set(['file2']);
+ element.reviewedFiles = new Set(['file2']);
element._path = 'file1';
nowStub.returns(5);
@@ -1846,7 +1850,7 @@
isAtStartStub.returns(true);
element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
- element._reviewedFiles = new Set(['file2']);
+ element.reviewedFiles = new Set(['file2']);
element._path = 'file3';
nowStub.returns(5);
@@ -1893,7 +1897,7 @@
test('shift+m navigates to next unreviewed file', () => {
element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
- element._reviewedFiles = new Set(['file1', 'file2']);
+ element.reviewedFiles = new Set(['file1', 'file2']);
element._path = 'file1';
const reviewedStub = sinon.stub(element, '_setReviewed');
const navStub = sinon.stub(element, '_navToFile');
@@ -2059,7 +2063,6 @@
stubRestApi('getConfig').returns(Promise.resolve({change: {}}));
stubRestApi('getProjectConfig').returns(Promise.resolve({}));
- stubRestApi('getDiffChangeDetail').returns(Promise.resolve({}));
stubRestApi('getChangeFiles').returns(Promise.resolve(changedFiles));
stubRestApi('saveFileReviewed').returns(Promise.resolve());
stubRestApi('getDiffComments').returns(Promise.resolve({}));
@@ -2069,7 +2072,6 @@
Promise.resolve([]));
element = basicFixture.instantiate();
element._changeNum = '42';
- return element._loadComments();
});
test('_getFiles add files with comments without changes', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.ts
index ba6fe7e..d072145 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.ts
@@ -15,7 +15,8 @@
* limitations under the License.
*/
import {BLANK_LINE, GrDiffLine, GrDiffLineType} from './gr-diff-line';
-import {LineRange} from '../../../api/diff';
+import {LineRange, Side} from '../../../api/diff';
+import {LineNumber} from './gr-diff-line';
export enum GrDiffGroupType {
/** Unchanged context. */
@@ -42,7 +43,7 @@
* originated from an `ab` chunk, or from an `a`+`b` chunk with
* `common: true`.
*
- * If the hidden range is 1 line or less, nothing is hidden and no context
+ * If the hidden range is 3 lines or less, nothing is hidden and no context
* control group is created.
*
* @param groups Common groups, ordered by their line ranges.
@@ -54,7 +55,7 @@
* start line, left and right respectively.
*/
export function hideInContextControl(
- groups: GrDiffGroup[],
+ groups: readonly GrDiffGroup[],
hiddenStart: number,
hiddenEnd: number
): GrDiffGroup[] {
@@ -65,7 +66,7 @@
let before: GrDiffGroup[] = [];
let hidden = groups;
- let after: GrDiffGroup[] = [];
+ let after: readonly GrDiffGroup[] = [];
const numHidden = hiddenEnd - hiddenStart;
@@ -90,9 +91,12 @@
const result = [...before];
if (hidden.length) {
- const ctxGroup = new GrDiffGroup(GrDiffGroupType.CONTEXT_CONTROL, []);
- ctxGroup.contextGroups = hidden;
- result.push(ctxGroup);
+ result.push(
+ new GrDiffGroup({
+ type: GrDiffGroupType.CONTEXT_CONTROL,
+ contextGroups: [...hidden],
+ })
+ );
}
result.push(...after);
return result;
@@ -173,7 +177,7 @@
* list of groups before and the list of groups after the split.
*/
function _splitCommonGroups(
- groups: GrDiffGroup[],
+ groups: readonly GrDiffGroup[],
split: number
): GrDiffGroup[][] {
if (groups.length === 0) return [[], []];
@@ -210,57 +214,131 @@
return [beforeGroups, afterGroups];
}
-/**
- * A chunk of the diff that should be rendered together.
- *
- * @constructor
- * @param {!GrDiffGroupType} type
- * @param {!Array<!GrDiffLine>=} opt_lines
- */
+export interface GrMoveDetails {
+ changed: boolean;
+ range?: {
+ start: number;
+ end: number;
+ };
+}
+
+/** A chunk of the diff that should be rendered together. */
export class GrDiffGroup {
- constructor(readonly type: GrDiffGroupType, lines: GrDiffLine[] = []) {
- lines.forEach((line: GrDiffLine) => this.addLine(line));
+ constructor(
+ options:
+ | {
+ type: GrDiffGroupType.BOTH | GrDiffGroupType.DELTA;
+ lines?: GrDiffLine[];
+ skip?: undefined;
+ moveDetails?: GrMoveDetails;
+ dueToRebase?: boolean;
+ ignoredWhitespaceOnly?: boolean;
+ keyLocation?: boolean;
+ }
+ | {
+ type: GrDiffGroupType.BOTH | GrDiffGroupType.DELTA;
+ lines?: undefined;
+ skip: number;
+ offsetLeft: number;
+ offsetRight: number;
+ moveDetails?: GrMoveDetails;
+ dueToRebase?: boolean;
+ ignoredWhitespaceOnly?: boolean;
+ keyLocation?: boolean;
+ }
+ | {
+ type: GrDiffGroupType.CONTEXT_CONTROL;
+ contextGroups: GrDiffGroup[];
+ }
+ ) {
+ this.type = options.type;
+ switch (options.type) {
+ case GrDiffGroupType.BOTH:
+ case GrDiffGroupType.DELTA: {
+ this.moveDetails = options.moveDetails;
+ this.dueToRebase = options.dueToRebase ?? false;
+ this.ignoredWhitespaceOnly = options.ignoredWhitespaceOnly ?? false;
+ this.keyLocation = options.keyLocation ?? false;
+ if (options.skip && options.lines) {
+ throw new Error('Cannot set skip and lines');
+ }
+ this.skip = options.skip;
+ if (options.skip) {
+ this.lineRange = {
+ left: {
+ start_line: options.offsetLeft,
+ end_line: options.offsetLeft + options.skip - 1,
+ },
+ right: {
+ start_line: options.offsetRight,
+ end_line: options.offsetRight + options.skip - 1,
+ },
+ };
+ } else {
+ for (const line of options.lines ?? []) {
+ this.addLine(line);
+ }
+ }
+ break;
+ }
+ case GrDiffGroupType.CONTEXT_CONTROL: {
+ this.contextGroups = options.contextGroups;
+ if (this.contextGroups.length > 0) {
+ const firstGroup = this.contextGroups[0];
+ const lastGroup = this.contextGroups[this.contextGroups.length - 1];
+ this.lineRange = {
+ left: {
+ start_line: firstGroup.lineRange.left.start_line,
+ end_line: lastGroup.lineRange.left.end_line,
+ },
+ right: {
+ start_line: firstGroup.lineRange.right.start_line,
+ end_line: lastGroup.lineRange.right.end_line,
+ },
+ };
+ }
+ break;
+ }
+ default:
+ throw new Error(`Unknown group type: ${this.type}`);
+ }
}
- dueToRebase = false;
+ readonly type: GrDiffGroupType;
+
+ readonly dueToRebase: boolean = false;
/**
* True means all changes in this line are whitespace changes that should
* not be highlighted as changed as per the user settings.
*/
- ignoredWhitespaceOnly = false;
+ readonly ignoredWhitespaceOnly: boolean = false;
/**
* True means it should not be collapsed (because it was in the URL, or
* there is a comment on that line)
*/
- keyLocation = false;
+ readonly keyLocation: boolean = false;
element?: HTMLElement;
- lines: GrDiffLine[] = [];
+ readonly lines: GrDiffLine[] = [];
- adds: GrDiffLine[] = [];
+ readonly adds: GrDiffLine[] = [];
- removes: GrDiffLine[] = [];
+ readonly removes: GrDiffLine[] = [];
- contextGroups: GrDiffGroup[] = [];
+ readonly contextGroups: GrDiffGroup[] = [];
- skip?: number;
+ readonly skip?: number;
/** Both start and end line are inclusive. */
- lineRange = {
- left: {start_line: 0, end_line: 0} as LineRange,
- right: {start_line: 0, end_line: 0} as LineRange,
+ readonly lineRange: {[side in Side]: LineRange} = {
+ [Side.LEFT]: {start_line: 0, end_line: 0},
+ [Side.RIGHT]: {start_line: 0, end_line: 0},
};
- moveDetails?: {
- changed: boolean;
- range?: {
- start: number;
- end: number;
- };
- };
+ readonly moveDetails?: GrMoveDetails;
/**
* Creates a new group with the same properties but different lines.
@@ -269,13 +347,22 @@
* rendering of the old lines, so that would not make sense.
*/
cloneWithLines(lines: GrDiffLine[]): GrDiffGroup {
- const group = new GrDiffGroup(this.type, lines);
- group.dueToRebase = this.dueToRebase;
- group.ignoredWhitespaceOnly = this.ignoredWhitespaceOnly;
+ if (
+ this.type !== GrDiffGroupType.BOTH &&
+ this.type !== GrDiffGroupType.DELTA
+ ) {
+ throw new Error('Cannot clone context group with lines');
+ }
+ const group = new GrDiffGroup({
+ type: this.type,
+ lines,
+ dueToRebase: this.dueToRebase,
+ ignoredWhitespaceOnly: this.ignoredWhitespaceOnly,
+ });
return group;
}
- addLine(line: GrDiffLine) {
+ private addLine(line: GrDiffLine) {
this.lines.push(line);
const notDelta =
@@ -293,7 +380,7 @@
} else if (line.type === GrDiffLineType.REMOVE) {
this.removes.push(line);
}
- this._updateRange(line);
+ this._updateRangeWithNewLine(line);
}
getSideBySidePairs(): GrDiffLinePair[] {
@@ -323,7 +410,21 @@
return pairs;
}
- _updateRange(line: GrDiffLine) {
+ /** Returns true if it is, or contains, a skip group. */
+ hasSkipGroup() {
+ return !!this.skip || this.contextGroups?.some(g => !!g.skip);
+ }
+
+ containsLine(side: Side, line: LineNumber) {
+ if (line === 'FILE' || line === 'LOST') {
+ // For FILE and LOST, beforeNumber and afterNumber are the same
+ return this.lines[0]?.beforeNumber === line;
+ }
+ const lineRange = this.lineRange[side];
+ return lineRange.start_line <= line && line <= lineRange.end_line;
+ }
+
+ private _updateRangeWithNewLine(line: GrDiffLine) {
if (
line.beforeNumber === 'FILE' ||
line.afterNumber === 'FILE' ||
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.js
index 4c7d346..73c12bf 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.js
@@ -21,13 +21,12 @@
suite('gr-diff-group tests', () => {
test('delta line pairs', () => {
- let group = new GrDiffGroup(GrDiffGroupType.DELTA);
const l1 = new GrDiffLine(GrDiffLineType.ADD, 0, 128);
const l2 = new GrDiffLine(GrDiffLineType.ADD, 0, 129);
const l3 = new GrDiffLine(GrDiffLineType.REMOVE, 64, 0);
- group.addLine(l1);
- group.addLine(l2);
- group.addLine(l3);
+ let group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines: [
+ l1, l2, l3,
+ ]});
assert.deepEqual(group.lines, [l1, l2, l3]);
assert.deepEqual(group.adds, [l1, l2]);
assert.deepEqual(group.removes, [l3]);
@@ -42,7 +41,7 @@
{left: BLANK_LINE, right: l2},
]);
- group = new GrDiffGroup(GrDiffGroupType.DELTA, [l1, l2, l3]);
+ group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines: [l1, l2, l3]});
assert.deepEqual(group.lines, [l1, l2, l3]);
assert.deepEqual(group.adds, [l1, l2]);
assert.deepEqual(group.removes, [l3]);
@@ -59,7 +58,8 @@
const l2 = new GrDiffLine(GrDiffLineType.BOTH, 65, 129);
const l3 = new GrDiffLine(GrDiffLineType.BOTH, 66, 130);
- let group = new GrDiffGroup(GrDiffGroupType.BOTH, [l1, l2, l3]);
+ const group = new GrDiffGroup({
+ type: GrDiffGroupType.BOTH, lines: [l1, l2, l3]});
assert.deepEqual(group.lines, [l1, l2, l3]);
assert.deepEqual(group.adds, []);
@@ -70,19 +70,7 @@
right: {start_line: 128, end_line: 130},
});
- let pairs = group.getSideBySidePairs();
- assert.deepEqual(pairs, [
- {left: l1, right: l1},
- {left: l2, right: l2},
- {left: l3, right: l3},
- ]);
-
- group = new GrDiffGroup(GrDiffGroupType.CONTEXT_CONTROL, [l1, l2, l3]);
- assert.deepEqual(group.lines, [l1, l2, l3]);
- assert.deepEqual(group.adds, []);
- assert.deepEqual(group.removes, []);
-
- pairs = group.getSideBySidePairs();
+ const pairs = group.getSideBySidePairs();
assert.deepEqual(pairs, [
{left: l1, right: l1},
{left: l2, right: l2},
@@ -95,27 +83,20 @@
const l2 = new GrDiffLine(GrDiffLineType.REMOVE);
const l3 = new GrDiffLine(GrDiffLineType.BOTH);
- let group = new GrDiffGroup(GrDiffGroupType.BOTH);
- assert.throws(group.addLine.bind(group, l1));
- assert.throws(group.addLine.bind(group, l2));
- assert.doesNotThrow(group.addLine.bind(group, l3));
-
- group = new GrDiffGroup(GrDiffGroupType.CONTEXT_CONTROL);
- assert.throws(group.addLine.bind(group, l1));
- assert.throws(group.addLine.bind(group, l2));
- assert.doesNotThrow(group.addLine.bind(group, l3));
+ assert.throws(() =>
+ new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [l1, l2, l3]}));
});
suite('hideInContextControl', () => {
let groups;
setup(() => {
groups = [
- new GrDiffGroup(GrDiffGroupType.BOTH, [
+ new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [
new GrDiffLine(GrDiffLineType.BOTH, 5, 7),
new GrDiffLine(GrDiffLineType.BOTH, 6, 8),
new GrDiffLine(GrDiffLineType.BOTH, 7, 9),
- ]),
- new GrDiffGroup(GrDiffGroupType.DELTA, [
+ ]}),
+ new GrDiffGroup({type: GrDiffGroupType.DELTA, lines: [
new GrDiffLine(GrDiffLineType.REMOVE, 8),
new GrDiffLine(GrDiffLineType.ADD, 0, 10),
new GrDiffLine(GrDiffLineType.REMOVE, 9),
@@ -124,12 +105,12 @@
new GrDiffLine(GrDiffLineType.ADD, 0, 12),
new GrDiffLine(GrDiffLineType.REMOVE, 11),
new GrDiffLine(GrDiffLineType.ADD, 0, 13),
- ]),
- new GrDiffGroup(GrDiffGroupType.BOTH, [
+ ]}),
+ new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [
new GrDiffLine(GrDiffLineType.BOTH, 12, 14),
new GrDiffLine(GrDiffLineType.BOTH, 13, 15),
new GrDiffLine(GrDiffLineType.BOTH, 14, 16),
- ]),
+ ]}),
];
});
@@ -181,24 +162,23 @@
suite('with skip chunks', () => {
setup(() => {
- const skipGroup = new GrDiffGroup(GrDiffGroupType.BOTH);
- skipGroup.skip = 60;
- skipGroup.lineRange = {
- left: {start_line: 8, end_line: 67},
- right: {start_line: 10, end_line: 69},
- };
+ const skipGroup = new GrDiffGroup({
+ type: GrDiffGroupType.BOTH,
+ skip: 60,
+ offsetLeft: 8,
+ offsetRight: 10});
groups = [
- new GrDiffGroup(GrDiffGroupType.BOTH, [
+ new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [
new GrDiffLine(GrDiffLineType.BOTH, 5, 7),
new GrDiffLine(GrDiffLineType.BOTH, 6, 8),
new GrDiffLine(GrDiffLineType.BOTH, 7, 9),
- ]),
+ ]}),
skipGroup,
- new GrDiffGroup(GrDiffGroupType.BOTH, [
+ new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [
new GrDiffLine(GrDiffLineType.BOTH, 68, 70),
new GrDiffLine(GrDiffLineType.BOTH, 69, 71),
new GrDiffLine(GrDiffLineType.BOTH, 70, 72),
- ]),
+ ]}),
];
});
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts
index 7393606..63db013 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-utils.ts
@@ -124,7 +124,7 @@
// For Gerrit these are instances of GrCommentThread, but other gr-diff users
// have different HTML elements in use for comment threads.
// TODO: Also document the required HTML attributes that thread elements must
-// have, e.g. 'diff-side', 'range', 'line-num', 'data-value'.
+// have, e.g. 'diff-side', 'range', 'line-num'.
export interface GrDiffThreadElement extends HTMLElement {
rootId: string;
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
index 6003a2f..030275f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
@@ -78,12 +78,12 @@
CreateCommentEventDetail as CreateCommentEventDetailApi,
RenderPreferences,
GrDiff as GrDiffApi,
+ DisplayLine,
} from '../../../api/diff';
import {isSafari, toggleClass} from '../../../utils/dom-util';
import {assertIsDefined} from '../../../utils/common-util';
import {debounce, DelayedTask} from '../../../utils/async-util';
import {
- DiffContextExpandedEventDetail,
getResponsiveMode,
isResponsive,
} from '../gr-diff-builder/gr-diff-builder';
@@ -104,11 +104,6 @@
*/
const COMMIT_MSG_LINE_LENGTH = 72;
-export interface LineOfInterest {
- number: number;
- leftSide: boolean;
-}
-
export interface GrDiff {
$: {
highlights: GrDiffHighlight;
@@ -203,8 +198,8 @@
@property({type: String, observer: '_viewModeObserver'})
viewMode = DiffViewMode.SIDE_BY_SIDE;
- @property({type: Object})
- lineOfInterest?: LineOfInterest;
+ @property({type: Object, observer: '_lineOfInterestObserver'})
+ lineOfInterest?: DisplayLine;
/**
* True when diff is changed, until the content is done rendering.
@@ -463,8 +458,8 @@
_computeKeyLocations() {
const keyLocations: KeyLocations = {left: {}, right: {}};
if (this.lineOfInterest) {
- const side = this.lineOfInterest.leftSide ? Side.LEFT : Side.RIGHT;
- keyLocations[side][this.lineOfInterest.number] = true;
+ const side = this.lineOfInterest.side;
+ keyLocations[side][this.lineOfInterest.lineNum] = true;
}
const threadEls = (dom(this) as PolymerDomWrapper)
.getEffectiveChildNodes()
@@ -547,11 +542,6 @@
return classes.join(' ');
}
- _handleDiffContextExpanded(e: CustomEvent<DiffContextExpandedEventDetail>) {
- // Don't stop propagation. The host may listen for reporting or resizing.
- this.$.diffBuilder.showContext(e.detail.groups, e.detail.section);
- }
-
_handleTap(e: CustomEvent) {
const el = (dom(e) as EventApi).localTarget as Element;
@@ -716,6 +706,14 @@
this._prefsChanged(this.prefs);
}
+ _lineOfInterestObserver() {
+ if (this.loading) return;
+ if (!this.lineOfInterest) return;
+ const lineNum = this.lineOfInterest.lineNum;
+ if (typeof lineNum !== 'number') return;
+ this.$.diffBuilder.unhideLine(lineNum, this.lineOfInterest.side);
+ }
+
_cleanup() {
this.cancel();
this.blame = null;
@@ -862,18 +860,17 @@
this._showWarning = false;
const keyLocations = this._computeKeyLocations();
- const bypassPrefs = this._getBypassPrefs(this.prefs);
- this.$.diffBuilder
- .render(keyLocations, bypassPrefs, this.renderPrefs)
- .then(() => {
- this.dispatchEvent(
- new CustomEvent('render', {
- bubbles: true,
- composed: true,
- detail: {contentRendered: true},
- })
- );
- });
+ this.$.diffBuilder.prefs = this._getBypassPrefs(this.prefs);
+ this.$.diffBuilder.renderPrefs = this.renderPrefs;
+ this.$.diffBuilder.render(keyLocations).then(() => {
+ this.dispatchEvent(
+ new CustomEvent('render', {
+ bubbles: true,
+ composed: true,
+ detail: {contentRendered: true},
+ })
+ );
+ });
}
_handleRenderContent() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
index 67b7a9f..d288008 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
@@ -570,7 +570,6 @@
<div
class$="[[_computeContainerClass(loggedIn, viewMode, displayLine)]]"
on-click="_handleTap"
- on-diff-context-expanded="_handleDiffContextExpanded"
>
<gr-diff-selection diff="[[diff]]">
<gr-diff-highlight
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
index 9ee779c..ef3ca39 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
@@ -23,6 +23,7 @@
import {_setHiddenScroll} from '../../../scripts/hiddenscroll.js';
import {runA11yAudit} from '../../../test/a11y-test-utils.js';
import '@polymer/paper-button/paper-button.js';
+import {Side} from '../../../api/diff.js';
import {mockPromise, stubRestApi} from '../../../test/test-utils.js';
const basicFixture = fixtureFromElement('gr-diff');
@@ -182,9 +183,9 @@
element.changeNum = 123;
element.patchRange = {basePatchNum: 1, patchNum: 2};
element.path = 'file.txt';
-
- element.$.diffBuilder._builder = element.$.diffBuilder._getDiffBuilder(
- getMockDiffResponse(), {...MINIMAL_PREFS});
+ element.$.diffBuilder.diff = getMockDiffResponse();
+ element.$.diffBuilder.prefs = {...MINIMAL_PREFS};
+ element.$.diffBuilder._builder = element.$.diffBuilder._getDiffBuilder();
// No thread groups.
assert.isNotOk(element._getThreadGroupForLine(contentEl));
@@ -496,21 +497,6 @@
await promise;
});
- test('_handleTap context', async () => {
- const showContextStub =
- sinon.stub(element.$.diffBuilder, 'showContext');
- const el = document.createElement('div');
- el.className = 'showContext';
- const promise = mockPromise();
- el.addEventListener('click', e => {
- element._handleDiffContextExpanded(e);
- assert.isTrue(showContextStub.called);
- promise.resolve();
- });
- el.click();
- await promise;
- });
-
test('_handleTap content', async () => {
const content = document.createElement('div');
const lineEl = document.createElement('div');
@@ -858,7 +844,7 @@
assert.equal(element.prefs.context, 3);
assert.equal(element._safetyBypass, -1);
- assert.equal(renderStub.firstCall.args[1].context, -1);
+ assert.equal(element.$.diffBuilder.prefs.context, -1);
});
test('toggles collapse context from bypass', async () => {
@@ -871,7 +857,7 @@
assert.equal(element.prefs.context, 3);
assert.isNull(element._safetyBypass);
- assert.equal(renderStub.firstCall.args[1].context, 3);
+ assert.equal(element.$.diffBuilder.prefs.context, 3);
});
test('toggles collapse context from pref using default', async () => {
@@ -883,7 +869,7 @@
assert.equal(element.prefs.context, -1);
assert.equal(element._safetyBypass, 10);
- assert.equal(renderStub.firstCall.args[1].context, 10);
+ assert.equal(element.$.diffBuilder.prefs.context, 10);
});
});
@@ -1000,7 +986,7 @@
});
test('lineOfInterest is a key location', () => {
- element.lineOfInterest = {number: 789, leftSide: true};
+ element.lineOfInterest = {lineNum: 789, side: Side.LEFT};
element._renderDiffTable();
assert.isTrue(renderStub.called);
assert.deepEqual(renderStub.lastCall.args[0], {
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
index 857ffa2..eeb636f 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
@@ -14,15 +14,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/gr-a11y-styles';
-import '../../../styles/shared-styles';
import '../../shared/gr-dropdown-list/gr-dropdown-list';
import '../../shared/gr-select/gr-select';
-import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-patch-range-select_html';
-import {pluralize} from '../../../utils/string-util';
-import {appContext} from '../../../services/app-context';
+import {convertToString, pluralize} from '../../../utils/string-util';
+import {getAppContext} from '../../../services/app-context';
import {
computeLatestPatchNum,
findSortedIndex,
@@ -33,7 +28,6 @@
PatchSet,
convertToPatchSetNum,
} from '../../../utils/patch-set-util';
-import {customElement, property, observe} from '@polymer/decorators';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
import {hasOwnProperty} from '../../../utils/common-util';
import {
@@ -44,7 +38,6 @@
Timestamp,
} from '../../../types/common';
import {RevisionInfo as RevisionInfoClass} from '../../shared/revision-info/revision-info';
-import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
import {ChangeComments} from '../gr-comment-api/gr-comment-api';
import {
DropdownItem,
@@ -52,10 +45,23 @@
GrDropdownList,
} from '../../shared/gr-dropdown-list/gr-dropdown-list';
import {GeneratedWebLink} from '../../core/gr-navigation/gr-navigation';
+import {EditRevisionInfo} from '../../../types/types';
+import {a11yStyles} from '../../../styles/gr-a11y-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, css, html} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
+import {subscribe} from '../../lit/subscription-controller';
+import {commentsModelToken} from '../../../models/comments/comments-model';
+import {resolve} from '../../../models/dependency';
+import {ifDefined} from 'lit/directives/if-defined';
// Maximum length for patch set descriptions.
const PATCH_DESC_MAX_LENGTH = 500;
+function getShaForPatch(patch: PatchSet) {
+ return patch.sha.substring(0, 10);
+}
+
export interface PatchRangeChangeDetail {
patchNum?: PatchSetNum;
basePatchNum?: BasePatchSetNum;
@@ -68,10 +74,13 @@
meta_b: GeneratedWebLink[];
}
-export interface GrPatchRangeSelect {
- $: {
- patchNumDropdown: GrDropdownList;
- };
+declare global {
+ interface HTMLElementEventMap {
+ 'value-change': DropDownValueChangeEvent;
+ }
+ interface HTMLElementTagNameMap {
+ 'gr-patch-range-select': GrPatchRangeSelect;
+ }
}
/**
@@ -83,37 +92,17 @@
* @property {string} basePatchNum
*/
@customElement('gr-patch-range-select')
-export class GrPatchRangeSelect extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
+export class GrPatchRangeSelect extends LitElement {
+ @query('#patchNumDropdown')
+ patchNumDropdown?: GrDropdownList;
@property({type: Array})
availablePatches?: PatchSet[];
- @property({
- type: Object,
- computed:
- '_computeBaseDropdownContent(availablePatches, patchNum,' +
- '_sortedRevisions, changeComments, revisionInfo)',
- })
- _baseDropdownContent?: DropdownItem[];
-
- @property({
- type: Object,
- computed:
- '_computePatchDropdownContent(availablePatches,' +
- 'basePatchNum, _sortedRevisions, changeComments)',
- })
- _patchDropdownContent?: DropdownItem[];
-
@property({type: String})
changeNum?: string;
@property({type: Object})
- changeComments?: ChangeComments;
-
- @property({type: Object})
filesWeblinks?: FilesWebLinks;
@property({type: String})
@@ -122,68 +111,144 @@
@property({type: String})
basePatchNum?: BasePatchSetNum;
+ /** Not used directly. Translated into `sortedRevisions` in willUpdate(). */
@property({type: Object})
- revisions?: RevisionInfo[];
+ revisions: (RevisionInfo | EditRevisionInfo)[] = [];
@property({type: Object})
revisionInfo?: RevisionInfoClass;
- @property({type: Array})
- _sortedRevisions?: RevisionInfo[];
+ /** Private internal state, derived from `revisions` in willUpdate(). */
+ @state()
+ private sortedRevisions: (RevisionInfo | EditRevisionInfo)[] = [];
- private readonly reporting: ReportingService = appContext.reportingService;
+ /** Private internal state, visible for testing. */
+ @state()
+ changeComments?: ChangeComments;
- constructor() {
- super();
- this.reporting = appContext.reportingService;
+ private readonly reporting: ReportingService =
+ getAppContext().reportingService;
+
+ private readonly getCommentsModel = resolve(this, commentsModelToken);
+
+ override connectedCallback() {
+ super.connectedCallback();
+ subscribe(
+ this,
+ this.getCommentsModel().changeComments$,
+ x => (this.changeComments = x)
+ );
}
- _getShaForPatch(patch: PatchSet) {
- return patch.sha.substring(0, 10);
+ static override get styles() {
+ return [
+ a11yStyles,
+ sharedStyles,
+ css`
+ :host {
+ align-items: center;
+ display: flex;
+ }
+ select {
+ max-width: 15em;
+ }
+ .arrow {
+ color: var(--deemphasized-text-color);
+ margin: 0 var(--spacing-m);
+ }
+ gr-dropdown-list {
+ --trigger-style-text-color: var(--deemphasized-text-color);
+ --trigger-style-font-family: var(--font-family);
+ }
+ @media screen and (max-width: 50em) {
+ .filesWeblinks {
+ display: none;
+ }
+ gr-dropdown-list {
+ --native-select-style: {
+ max-width: 5.25em;
+ }
+ }
+ }
+ `,
+ ];
}
- _computeBaseDropdownContent(
- availablePatches?: PatchSet[],
- patchNum?: PatchSetNum,
- _sortedRevisions?: RevisionInfo[],
- changeComments?: ChangeComments,
- revisionInfo?: RevisionInfoClass
- ): DropdownItem[] | undefined {
- // Polymer 2: check for undefined
+ override render() {
+ return html`
+ <h3 class="assistive-tech-only">Patchset Range Selection</h3>
+ <span class="patchRange" aria-label="patch range starts with">
+ <gr-dropdown-list
+ id="basePatchDropdown"
+ .value="${convertToString(this.basePatchNum)}"
+ .items="${this.computeBaseDropdownContent()}"
+ @value-change=${this.handlePatchChange}
+ >
+ </gr-dropdown-list>
+ </span>
+ ${this.renderWeblinks(this.filesWeblinks?.meta_a)}
+ <span aria-hidden="true" class="arrow">→</span>
+ <span class="patchRange" aria-label="patch range ends with">
+ <gr-dropdown-list
+ id="patchNumDropdown"
+ .value="${convertToString(this.patchNum)}"
+ .items="${this.computePatchDropdownContent()}"
+ @value-change=${this.handlePatchChange}
+ >
+ </gr-dropdown-list>
+ ${this.renderWeblinks(this.filesWeblinks?.meta_b)}
+ </span>
+ `;
+ }
+
+ private renderWeblinks(fileLinks?: GeneratedWebLink[]) {
+ if (!fileLinks) return;
+ return html`<span class="filesWeblinks">
+ ${fileLinks.map(
+ weblink => html`
+ <a target="_blank" rel="noopener" href="${ifDefined(weblink.url)}">
+ ${weblink.name}
+ </a>
+ `
+ )}</span
+ > `;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('revisions')) {
+ this.sortedRevisions = sortRevisions(Object.values(this.revisions || {}));
+ }
+ }
+
+ // Private method, but visible for testing.
+ computeBaseDropdownContent(): DropdownItem[] {
if (
- availablePatches === undefined ||
- patchNum === undefined ||
- _sortedRevisions === undefined ||
- changeComments === undefined ||
- revisionInfo === undefined
+ this.availablePatches === undefined ||
+ this.patchNum === undefined ||
+ this.changeComments === undefined ||
+ this.revisionInfo === undefined
) {
- return undefined;
+ return [];
}
- const parentCounts = revisionInfo.getParentCountMap();
- const currentParentCount = hasOwnProperty(parentCounts, patchNum)
- ? parentCounts[patchNum as number]
+ const parentCounts = this.revisionInfo.getParentCountMap();
+ const currentParentCount = hasOwnProperty(parentCounts, this.patchNum)
+ ? parentCounts[this.patchNum as number]
: 1;
- const maxParents = revisionInfo.getMaxParents();
+ const maxParents = this.revisionInfo.getMaxParents();
const isMerge = currentParentCount > 1;
const dropdownContent: DropdownItem[] = [];
- for (const basePatch of availablePatches) {
+ for (const basePatch of this.availablePatches) {
const basePatchNum = basePatch.num;
- const entry: DropdownItem = this._createDropdownEntry(
+ const entry: DropdownItem = this.createDropdownEntry(
basePatchNum,
'Patchset ',
- _sortedRevisions,
- changeComments,
- this._getShaForPatch(basePatch)
+ getShaForPatch(basePatch)
);
dropdownContent.push({
...entry,
- disabled: this._computeLeftDisabled(
- basePatch.num,
- patchNum,
- _sortedRevisions
- ),
+ disabled: this.computeLeftDisabled(basePatch.num, this.patchNum),
});
}
@@ -205,122 +270,84 @@
return dropdownContent;
}
- _computeMobileText(
- patchNum: PatchSetNum,
- changeComments: ChangeComments,
- revisions: RevisionInfo[]
- ) {
+ private computeMobileText(patchNum: PatchSetNum) {
return (
`${patchNum}` +
- `${this._computePatchSetCommentsString(changeComments, patchNum)}` +
- `${this._computePatchSetDescription(revisions, patchNum, true)}`
+ `${this.computePatchSetCommentsString(patchNum)}` +
+ `${this.computePatchSetDescription(patchNum, true)}`
);
}
- _computePatchDropdownContent(
- availablePatches?: PatchSet[],
- basePatchNum?: BasePatchSetNum,
- _sortedRevisions?: RevisionInfo[],
- changeComments?: ChangeComments
- ): DropdownItem[] | undefined {
- // Polymer 2: check for undefined
+ // Private method, but visible for testing.
+ computePatchDropdownContent(): DropdownItem[] {
if (
- availablePatches === undefined ||
- basePatchNum === undefined ||
- _sortedRevisions === undefined ||
- changeComments === undefined
+ this.availablePatches === undefined ||
+ this.basePatchNum === undefined ||
+ this.changeComments === undefined
) {
- return undefined;
+ return [];
}
const dropdownContent: DropdownItem[] = [];
- for (const patch of availablePatches) {
+ for (const patch of this.availablePatches) {
const patchNum = patch.num;
- const entry = this._createDropdownEntry(
+ const entry = this.createDropdownEntry(
patchNum,
patchNum === 'edit' ? '' : 'Patchset ',
- _sortedRevisions,
- changeComments,
- this._getShaForPatch(patch)
+ getShaForPatch(patch)
);
dropdownContent.push({
...entry,
- disabled: this._computeRightDisabled(
- basePatchNum,
- patchNum,
- _sortedRevisions
- ),
+ disabled: this.computeRightDisabled(this.basePatchNum, patchNum),
});
}
return dropdownContent;
}
- _computeText(
- patchNum: PatchSetNum,
- prefix: string,
- changeComments: ChangeComments,
- sha: string
- ) {
+ private computeText(patchNum: PatchSetNum, prefix: string, sha: string) {
return (
`${prefix}${patchNum}` +
- `${this._computePatchSetCommentsString(changeComments, patchNum)}` +
+ `${this.computePatchSetCommentsString(patchNum)}` +
` | ${sha}`
);
}
- _createDropdownEntry(
+ private createDropdownEntry(
patchNum: PatchSetNum,
prefix: string,
- sortedRevisions: RevisionInfo[],
- changeComments: ChangeComments,
sha: string
) {
const entry: DropdownItem = {
triggerText: `${prefix}${patchNum}`,
- text: this._computeText(patchNum, prefix, changeComments, sha),
- mobileText: this._computeMobileText(
- patchNum,
- changeComments,
- sortedRevisions
- ),
- bottomText: `${this._computePatchSetDescription(
- sortedRevisions,
- patchNum
- )}`,
+ text: this.computeText(patchNum, prefix, sha),
+ mobileText: this.computeMobileText(patchNum),
+ bottomText: `${this.computePatchSetDescription(patchNum)}`,
value: patchNum,
};
- const date = this._computePatchSetDate(sortedRevisions, patchNum);
+ const date = this.computePatchSetDate(patchNum);
if (date) {
entry.date = date;
}
return entry;
}
- @observe('revisions.*')
- _updateSortedRevisions(
- revisionsRecord: PolymerDeepPropertyChange<RevisionInfo[], RevisionInfo[]>
- ) {
- const revisions = revisionsRecord.base;
- if (!revisions) return;
- this._sortedRevisions = sortRevisions(Object.values(revisions));
- }
-
/**
* The basePatchNum should always be <= patchNum -- because sortedRevisions
* is sorted in reverse order (higher patchset nums first), invalid base
* patch nums have an index greater than the index of patchNum.
*
+ * Private method, but visible for testing.
+ *
* @param basePatchNum The possible base patch num.
* @param patchNum The current selected patch num.
*/
- _computeLeftDisabled(
+ computeLeftDisabled(
basePatchNum: PatchSetNum,
- patchNum: PatchSetNum,
- sortedRevisions: RevisionInfo[]
+ patchNum: PatchSetNum
): boolean {
return (
- findSortedIndex(basePatchNum, sortedRevisions) <=
- findSortedIndex(patchNum, sortedRevisions)
+ findSortedIndex(basePatchNum, this.sortedRevisions) <=
+ findSortedIndex(patchNum, this.sortedRevisions)
);
}
@@ -335,13 +362,14 @@
* If the current basePatchNum is a parent index, then only patches that have
* at least that many parents are valid.
*
+ * Private method, but visible for testing.
+ *
* @param basePatchNum The current selected base patch num.
* @param patchNum The possible patch num.
*/
- _computeRightDisabled(
+ computeRightDisabled(
basePatchNum: PatchSetNum,
- patchNum: PatchSetNum,
- sortedRevisions: RevisionInfo[]
+ patchNum: PatchSetNum
): boolean {
if (basePatchNum === ParentPatchSetNum) {
return false;
@@ -359,21 +387,17 @@
}
return (
- findSortedIndex(basePatchNum, sortedRevisions) <=
- findSortedIndex(patchNum, sortedRevisions)
+ findSortedIndex(basePatchNum, this.sortedRevisions) <=
+ findSortedIndex(patchNum, this.sortedRevisions)
);
}
// TODO(dhruvsri): have ported comments contribute to this count
- _computePatchSetCommentsString(
- changeComments: ChangeComments,
- patchNum: PatchSetNum
- ) {
- if (!changeComments) {
- return;
- }
+ // Private method, but visible for testing.
+ computePatchSetCommentsString(patchNum: PatchSetNum): string {
+ if (!this.changeComments) return '';
- const commentThreadCount = changeComments.computeCommentThreadCount(
+ const commentThreadCount = this.changeComments.computeCommentThreadCount(
{
patchNum,
},
@@ -381,7 +405,7 @@
);
const commentThreadString = pluralize(commentThreadCount, 'comment');
- const unresolvedCount = changeComments.computeUnresolvedNum(
+ const unresolvedCount = this.changeComments.computeUnresolvedNum(
{patchNum},
true
);
@@ -400,23 +424,19 @@
);
}
- _computePatchSetDescription(
- revisions: RevisionInfo[],
+ private computePatchSetDescription(
patchNum: PatchSetNum,
addFrontSpace?: boolean
) {
- const rev = getRevisionByPatchNum(revisions, patchNum);
+ const rev = getRevisionByPatchNum(this.sortedRevisions, patchNum);
return rev?.description
? (addFrontSpace ? ' ' : '') +
rev.description.substring(0, PATCH_DESC_MAX_LENGTH)
: '';
}
- _computePatchSetDate(
- revisions: RevisionInfo[],
- patchNum: PatchSetNum
- ): Timestamp | undefined {
- const rev = getRevisionByPatchNum(revisions, patchNum);
+ private computePatchSetDate(patchNum: PatchSetNum): Timestamp | undefined {
+ const rev = getRevisionByPatchNum(this.sortedRevisions, patchNum);
return rev ? rev.created : undefined;
}
@@ -424,15 +444,15 @@
* Catches value-change events from the patchset dropdowns and determines
* whether or not a patch change event should be fired.
*/
- _handlePatchChange(e: DropDownValueChangeEvent) {
+ private handlePatchChange(e: DropDownValueChangeEvent) {
const detail: PatchRangeChangeDetail = {
patchNum: this.patchNum,
basePatchNum: this.basePatchNum,
};
- const target = (dom(e) as EventApi).localTarget;
+ const target = e.target;
const patchSetValue = convertToPatchSetNum(e.detail.value)!;
const latestPatchNum = computeLatestPatchNum(this.availablePatches);
- if (target === this.$.patchNumDropdown) {
+ if (target === this.patchNumDropdown) {
if (detail.patchNum === e.detail.value) return;
this.reporting.reportInteraction('right-patchset-changed', {
previous: detail.patchNum,
@@ -460,9 +480,3 @@
);
}
}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-patch-range-select': GrPatchRangeSelect;
- }
-}
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts
deleted file mode 100644
index 26944a4..0000000
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-a11y-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- :host {
- align-items: center;
- display: flex;
- }
- select {
- max-width: 15em;
- }
- .arrow {
- color: var(--deemphasized-text-color);
- margin: 0 var(--spacing-m);
- }
- gr-dropdown-list {
- --trigger-style-text-color: var(--deemphasized-text-color);
- --trigger-style-font-family: var(--font-family);
- }
- @media screen and (max-width: 50em) {
- .filesWeblinks {
- display: none;
- }
- gr-dropdown-list {
- --native-select-style: {
- max-width: 5.25em;
- }
- }
- }
- </style>
- <h3 class="assistive-tech-only">Patchset Range Selection</h3>
- <span class="patchRange" aria-label="patch range starts with">
- <gr-dropdown-list
- id="basePatchDropdown"
- value="[[basePatchNum]]"
- on-value-change="_handlePatchChange"
- items="[[_baseDropdownContent]]"
- >
- </gr-dropdown-list>
- </span>
- <span is="dom-if" if="[[filesWeblinks.meta_a]]" class="filesWeblinks">
- <template is="dom-repeat" items="[[filesWeblinks.meta_a]]" as="weblink">
- <a target="_blank" rel="noopener" href$="[[weblink.url]]"
- >[[weblink.name]]</a
- >
- </template>
- </span>
- <span aria-hidden="true" class="arrow">→</span>
- <span class="patchRange" aria-label="patch range ends with">
- <gr-dropdown-list
- id="patchNumDropdown"
- value="[[patchNum]]"
- on-value-change="_handlePatchChange"
- items="[[_patchDropdownContent]]"
- >
- </gr-dropdown-list>
- <span is="dom-if" if="[[filesWeblinks.meta_b]]" class="filesWeblinks">
- <template is="dom-repeat" items="[[filesWeblinks.meta_b]]" as="weblink">
- <a target="_blank" href$="[[weblink.url]]">[[weblink.name]]</a>
- </template>
- </span>
- </span>
-`;
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
deleted file mode 100644
index 28ebbac..0000000
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
+++ /dev/null
@@ -1,395 +0,0 @@
-/**
- * @license
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import '../gr-comment-api/gr-comment-api.js';
-import '../../shared/revision-info/revision-info.js';
-import './gr-patch-range-select.js';
-import '../../../test/mocks/comment-api.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {RevisionInfo} from '../../shared/revision-info/revision-info.js';
-import {createCommentApiMockWithTemplateElement} from '../../../test/mocks/comment-api';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-import {ChangeComments} from '../gr-comment-api/gr-comment-api.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-import {EditPatchSetNum} from '../../../types/common.js';
-import {SpecialFilePath} from '../../../constants/constants.js';
-
-const commentApiMockElement = createCommentApiMockWithTemplateElement(
- 'gr-patch-range-select-comment-api-mock', html`
- <gr-patch-range-select id="patchRange" auto
- change-comments="[[_changeComments]]"></gr-patch-range-select>
- <gr-comment-api id="commentAPI"></gr-comment-api>
-`);
-
-const basicFixture = fixtureFromElement(commentApiMockElement.is);
-
-suite('gr-patch-range-select tests', () => {
- let element;
-
- let commentApiWrapper;
-
- function getInfo(revisions) {
- const revisionObj = {};
- for (let i = 0; i < revisions.length; i++) {
- revisionObj[i] = revisions[i];
- }
- return new RevisionInfo({revisions: revisionObj});
- }
-
- setup(() => {
- stubRestApi('getDiffComments').returns(Promise.resolve({}));
- stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
- stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
-
- // Element must be wrapped in an element with direct access to the
- // comment API.
- commentApiWrapper = basicFixture.instantiate();
- element = commentApiWrapper.$.patchRange;
-
- // Stub methods on the changeComments object after changeComments has
- // been initialized.
- element.changeComments = new ChangeComments();
- });
-
- test('enabled/disabled options', () => {
- const patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 3,
- };
- const sortedRevisions = [
- {_number: 3},
- {_number: EditPatchSetNum, basePatchNum: 2},
- {_number: 2},
- {_number: 1},
- ];
- for (const patchNum of ['1', '2', '3']) {
- assert.isFalse(element._computeRightDisabled(patchRange.basePatchNum,
- patchNum, sortedRevisions));
- }
- for (const basePatchNum of ['1', '2']) {
- assert.isFalse(element._computeLeftDisabled(basePatchNum,
- patchRange.patchNum, sortedRevisions));
- }
- assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum));
-
- patchRange.basePatchNum = EditPatchSetNum;
- assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum,
- sortedRevisions));
- assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, '1',
- sortedRevisions));
- assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, '2',
- sortedRevisions));
- assert.isFalse(element._computeRightDisabled(patchRange.basePatchNum, '3',
- sortedRevisions));
- assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum,
- EditPatchSetNum, sortedRevisions));
- });
-
- test('_computeBaseDropdownContent', () => {
- const availablePatches = [
- {num: 'edit', sha: '1'},
- {num: 3, sha: '2'},
- {num: 2, sha: '3'},
- {num: 1, sha: '4'},
- ];
- const revisions = [
- {
- commit: {parents: []},
- _number: 2,
- description: 'description',
- },
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(revisions);
- const patchNum = 1;
- const sortedRevisions = [
- {_number: 3, created: 'Mon, 01 Jan 2001 00:00:00 GMT'},
- {_number: EditPatchSetNum, basePatchNum: 2},
- {_number: 2, description: 'description'},
- {_number: 1},
- ];
- const expectedResult = [
- {
- disabled: true,
- triggerText: 'Patchset edit',
- text: 'Patchset edit | 1',
- mobileText: 'edit',
- bottomText: '',
- value: 'edit',
- },
- {
- disabled: true,
- triggerText: 'Patchset 3',
- text: 'Patchset 3 | 2',
- mobileText: '3',
- bottomText: '',
- value: 3,
- date: 'Mon, 01 Jan 2001 00:00:00 GMT',
- },
- {
- disabled: true,
- triggerText: 'Patchset 2',
- text: 'Patchset 2 | 3',
- mobileText: '2 description',
- bottomText: 'description',
- value: 2,
- },
- {
- disabled: true,
- triggerText: 'Patchset 1',
- text: 'Patchset 1 | 4',
- mobileText: '1',
- bottomText: '',
- value: 1,
- },
- {
- text: 'Base',
- value: 'PARENT',
- },
- ];
- assert.deepEqual(element._computeBaseDropdownContent(availablePatches,
- patchNum, sortedRevisions, element.changeComments,
- element.revisionInfo),
- expectedResult);
- });
-
- test('_computeBaseDropdownContent called when patchNum updates', () => {
- element.revisions = [
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(element.revisions);
- element.availablePatches = [
- {num: 1, sha: '1'},
- {num: 2, sha: '2'},
- {num: 3, sha: '3'},
- {num: 'edit', sha: '4'},
- ];
- element.patchNum = 2;
- element.basePatchNum = 'PARENT';
- flush();
-
- sinon.stub(element, '_computeBaseDropdownContent');
-
- // Should be recomputed for each available patch
- element.set('patchNum', 1);
- assert.equal(element._computeBaseDropdownContent.callCount, 1);
- });
-
- test('_computeBaseDropdownContent called when changeComments update',
- async () => {
- element.revisions = [
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(element.revisions);
- element.availablePatches = [
- {num: 'edit', sha: '1'},
- {num: 3, sha: '2'},
- {num: 2, sha: '3'},
- {num: 1, sha: '4'},
- ];
- element.patchNum = 2;
- element.basePatchNum = 'PARENT';
- await flush();
-
- // Should be recomputed for each available patch
- sinon.stub(element, '_computeBaseDropdownContent');
- assert.equal(element._computeBaseDropdownContent.callCount, 0);
- element.changeComments = new ChangeComments();
- await flush();
- assert.equal(element._computeBaseDropdownContent.callCount, 1);
- });
-
- test('_computePatchDropdownContent called when basePatchNum updates', () => {
- element.revisions = [
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- {commit: {parents: []}},
- ];
- element.revisionInfo = getInfo(element.revisions);
- element.availablePatches = [
- {num: 1, sha: '1'},
- {num: 2, sha: '2'},
- {num: 3, sha: '3'},
- {num: 'edit', sha: '4'},
- ];
- element.patchNum = 2;
- element.basePatchNum = 'PARENT';
- flush();
-
- // Should be recomputed for each available patch
- sinon.stub(element, '_computePatchDropdownContent');
- element.set('basePatchNum', 1);
- assert.equal(element._computePatchDropdownContent.callCount, 1);
- });
-
- test('_computePatchDropdownContent', () => {
- const availablePatches = [
- {num: 'edit', sha: '1'},
- {num: 3, sha: '2'},
- {num: 2, sha: '3'},
- {num: 1, sha: '4'},
- ];
- const basePatchNum = 1;
- const sortedRevisions = [
- {_number: 3, created: 'Mon, 01 Jan 2001 00:00:00 GMT'},
- {_number: EditPatchSetNum, basePatchNum: 2},
- {_number: 2, description: 'description'},
- {_number: 1},
- ];
-
- const expectedResult = [
- {
- disabled: false,
- triggerText: 'edit',
- text: 'edit | 1',
- mobileText: 'edit',
- bottomText: '',
- value: 'edit',
- },
- {
- disabled: false,
- triggerText: 'Patchset 3',
- text: 'Patchset 3 | 2',
- mobileText: '3',
- bottomText: '',
- value: 3,
- date: 'Mon, 01 Jan 2001 00:00:00 GMT',
- },
- {
- disabled: false,
- triggerText: 'Patchset 2',
- text: 'Patchset 2 | 3',
- mobileText: '2 description',
- bottomText: 'description',
- value: 2,
- },
- {
- disabled: true,
- triggerText: 'Patchset 1',
- text: 'Patchset 1 | 4',
- mobileText: '1',
- bottomText: '',
- value: 1,
- },
- ];
-
- assert.deepEqual(element._computePatchDropdownContent(availablePatches,
- basePatchNum, sortedRevisions, element.changeComments),
- expectedResult);
- });
-
- test('filesWeblinks', () => {
- element.filesWeblinks = {
- meta_a: [
- {
- name: 'foo',
- url: 'f.oo',
- },
- ],
- meta_b: [
- {
- name: 'bar',
- url: 'ba.r',
- },
- ],
- };
- flush();
- const domApi = dom(element.root);
- assert.equal(
- domApi.querySelector('a[href="f.oo"]').textContent, 'foo');
- assert.equal(
- domApi.querySelector('a[href="ba.r"]').textContent, 'bar');
- });
-
- test('_computePatchSetCommentsString', () => {
- // Test string with unresolved comments.
- const comments = {
- foo: [{
- id: '27dcee4d_f7b77cfa',
- message: 'test',
- patch_set: 1,
- unresolved: true,
- updated: '2017-10-11 20:48:40.000000000',
- }],
- bar: [
- {
- id: '27dcee4d_f7b77cfa',
- message: 'test',
- patch_set: 1,
- updated: '2017-10-12 20:48:40.000000000',
- },
- {
- id: '27dcee4d_f7b77cfa',
- message: 'test',
- patch_set: 1,
- updated: '2017-10-13 20:48:40.000000000',
- },
- ],
- abc: [],
- // Patchset level comment does not contribute to the count
- [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [{
- id: '27dcee4d_f7b77cfa',
- message: 'test',
- patch_set: 1,
- unresolved: true,
- updated: '2017-10-11 20:48:40.000000000',
- }],
- };
- element.changeComments = new ChangeComments(comments);
-
- assert.equal(element._computePatchSetCommentsString(
- element.changeComments, 1), ' (3 comments, 1 unresolved)');
-
- // Test string with no unresolved comments.
- delete element.changeComments._comments['foo'];
- assert.equal(element._computePatchSetCommentsString(
- element.changeComments, 1), ' (2 comments)');
-
- // Test string with no comments.
- delete element.changeComments._comments['bar'];
- assert.equal(element._computePatchSetCommentsString(
- element.changeComments, 1), '');
- });
-
- test('patch-range-change fires', () => {
- const handler = sinon.stub();
- element.basePatchNum = 1;
- element.patchNum = 3;
- element.addEventListener('patch-range-change', handler);
-
- element.$.basePatchDropdown._handleValueChange(2, [{value: 2}]);
- assert.isTrue(handler.calledOnce);
- assert.deepEqual(handler.lastCall.args[0].detail,
- {basePatchNum: 2, patchNum: 3});
-
- // BasePatchNum should not have changed, due to one-way data binding.
- element.$.patchNumDropdown._handleValueChange('edit', [{value: 'edit'}]);
- assert.deepEqual(handler.lastCall.args[0].detail,
- {basePatchNum: 1, patchNum: 'edit'});
- });
-});
-
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
new file mode 100644
index 0000000..342fe3a
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
@@ -0,0 +1,421 @@
+/**
+ * @license
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import '../gr-comment-api/gr-comment-api';
+import '../../shared/revision-info/revision-info';
+import './gr-patch-range-select';
+import {GrPatchRangeSelect} from './gr-patch-range-select';
+import '../../../test/mocks/comment-api';
+import {RevisionInfo as RevisionInfoClass} from '../../shared/revision-info/revision-info';
+import {ChangeComments} from '../gr-comment-api/gr-comment-api';
+import {stubRestApi} from '../../../test/test-utils';
+import {
+ BasePatchSetNum,
+ EditPatchSetNum,
+ PatchSetNum,
+ RevisionInfo,
+ Timestamp,
+ UrlEncodedCommentId,
+ PathToCommentsInfoMap,
+} from '../../../types/common';
+import {EditRevisionInfo, ParsedChangeInfo} from '../../../types/types';
+import {SpecialFilePath} from '../../../constants/constants';
+import {
+ createEditRevision,
+ createRevision,
+} from '../../../test/test-data-generators';
+import {PatchSet} from '../../../utils/patch-set-util';
+import {
+ DropdownItem,
+ GrDropdownList,
+} from '../../shared/gr-dropdown-list/gr-dropdown-list';
+import {queryAndAssert} from '../../../test/test-utils';
+
+const basicFixture = fixtureFromElement('gr-patch-range-select');
+
+type RevIdToRevisionInfo = {
+ [revisionId: string]: RevisionInfo | EditRevisionInfo;
+};
+
+suite('gr-patch-range-select tests', () => {
+ let element: GrPatchRangeSelect;
+
+ function getInfo(revisions: (RevisionInfo | EditRevisionInfo)[]) {
+ const revisionObj: Partial<RevIdToRevisionInfo> = {};
+ for (let i = 0; i < revisions.length; i++) {
+ revisionObj[i] = revisions[i];
+ }
+ return new RevisionInfoClass({revisions: revisionObj} as ParsedChangeInfo);
+ }
+
+ setup(async () => {
+ stubRestApi('getDiffComments').returns(Promise.resolve({}));
+ stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
+ stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
+
+ // Element must be wrapped in an element with direct access to the
+ // comment API.
+ element = basicFixture.instantiate();
+
+ // Stub methods on the changeComments object after changeComments has
+ // been initialized.
+ element.changeComments = new ChangeComments();
+ await element.updateComplete;
+ });
+
+ test('enabled/disabled options', async () => {
+ element.revisions = [
+ createRevision(3) as RevisionInfo,
+ createEditRevision(2) as EditRevisionInfo,
+ createRevision(2) as RevisionInfo,
+ createRevision(1) as RevisionInfo,
+ ];
+ await element.updateComplete;
+
+ const parent = 'PARENT' as PatchSetNum;
+ const edit = EditPatchSetNum;
+
+ for (const patchNum of [1, 2, 3]) {
+ assert.isFalse(
+ element.computeRightDisabled(parent, patchNum as PatchSetNum)
+ );
+ }
+ for (const basePatchNum of [1, 2]) {
+ const base = basePatchNum as PatchSetNum;
+ assert.isFalse(element.computeLeftDisabled(base, 3 as PatchSetNum));
+ }
+ assert.isTrue(
+ element.computeLeftDisabled(3 as PatchSetNum, 3 as PatchSetNum)
+ );
+
+ assert.isTrue(
+ element.computeLeftDisabled(3 as PatchSetNum, 3 as PatchSetNum)
+ );
+ assert.isTrue(element.computeRightDisabled(edit, 1 as PatchSetNum));
+ assert.isTrue(element.computeRightDisabled(edit, 2 as PatchSetNum));
+ assert.isFalse(element.computeRightDisabled(edit, 3 as PatchSetNum));
+ assert.isTrue(element.computeRightDisabled(edit, edit));
+ });
+
+ test('computeBaseDropdownContent', async () => {
+ element.availablePatches = [
+ {num: 'edit', sha: '1'} as PatchSet,
+ {num: 3, sha: '2'} as PatchSet,
+ {num: 2, sha: '3'} as PatchSet,
+ {num: 1, sha: '4'} as PatchSet,
+ ];
+ element.revisions = [
+ createRevision(2),
+ createRevision(3),
+ createRevision(1),
+ createRevision(4),
+ ];
+ element.revisionInfo = getInfo(element.revisions);
+ const expectedResult: DropdownItem[] = [
+ {
+ disabled: true,
+ triggerText: 'Patchset edit',
+ text: 'Patchset edit | 1',
+ mobileText: 'edit',
+ bottomText: '',
+ value: 'edit',
+ },
+ {
+ disabled: true,
+ triggerText: 'Patchset 3',
+ text: 'Patchset 3 | 2',
+ mobileText: '3',
+ bottomText: '',
+ value: 3,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ } as DropdownItem,
+ {
+ disabled: true,
+ triggerText: 'Patchset 2',
+ text: 'Patchset 2 | 3',
+ mobileText: '2',
+ bottomText: '',
+ value: 2,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ } as DropdownItem,
+ {
+ disabled: true,
+ triggerText: 'Patchset 1',
+ text: 'Patchset 1 | 4',
+ mobileText: '1',
+ bottomText: '',
+ value: 1,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ } as DropdownItem,
+ {
+ text: 'Base',
+ value: 'PARENT',
+ } as DropdownItem,
+ ];
+ element.patchNum = 1 as PatchSetNum;
+ element.basePatchNum = 'PARENT' as BasePatchSetNum;
+ await element.updateComplete;
+
+ assert.deepEqual(element.computeBaseDropdownContent(), expectedResult);
+ });
+
+ test('computeBaseDropdownContent called when patchNum updates', async () => {
+ element.revisions = [
+ createRevision(2),
+ createRevision(3),
+ createRevision(1),
+ createRevision(4),
+ ];
+ element.revisionInfo = getInfo(element.revisions);
+ element.availablePatches = [
+ {num: 1, sha: '1'} as PatchSet,
+ {num: 2, sha: '2'} as PatchSet,
+ {num: 3, sha: '3'} as PatchSet,
+ {num: 'edit', sha: '4'} as PatchSet,
+ ];
+ element.patchNum = 2 as PatchSetNum;
+ element.basePatchNum = 'PARENT' as BasePatchSetNum;
+ await element.updateComplete;
+
+ const baseDropDownStub = sinon.stub(element, 'computeBaseDropdownContent');
+
+ // Should be recomputed for each available patch
+ element.patchNum = 1 as PatchSetNum;
+ await element.updateComplete;
+ assert.equal(baseDropDownStub.callCount, 1);
+ });
+
+ test('computeBaseDropdownContent called when changeComments update', async () => {
+ element.revisions = [
+ createRevision(2),
+ createRevision(3),
+ createRevision(1),
+ createRevision(4),
+ ];
+ element.revisionInfo = getInfo(element.revisions);
+ element.availablePatches = [
+ {num: 3, sha: '2'} as PatchSet,
+ {num: 2, sha: '3'} as PatchSet,
+ {num: 1, sha: '4'} as PatchSet,
+ ];
+ element.patchNum = 2 as PatchSetNum;
+ element.basePatchNum = 'PARENT' as BasePatchSetNum;
+ await element.updateComplete;
+
+ // Should be recomputed for each available patch
+ const baseDropDownStub = sinon.stub(element, 'computeBaseDropdownContent');
+ assert.equal(baseDropDownStub.callCount, 0);
+ element.changeComments = new ChangeComments();
+ await element.updateComplete;
+ assert.equal(baseDropDownStub.callCount, 1);
+ });
+
+ test('computePatchDropdownContent called when basePatchNum updates', async () => {
+ element.revisions = [
+ createRevision(2),
+ createRevision(3),
+ createRevision(1),
+ createRevision(4),
+ ];
+ element.revisionInfo = getInfo(element.revisions);
+ element.availablePatches = [
+ {num: 1, sha: '1'} as PatchSet,
+ {num: 2, sha: '2'} as PatchSet,
+ {num: 3, sha: '3'} as PatchSet,
+ {num: 'edit', sha: '4'} as PatchSet,
+ ];
+ element.patchNum = 2 as PatchSetNum;
+ element.basePatchNum = 'PARENT' as BasePatchSetNum;
+ await element.updateComplete;
+
+ // Should be recomputed for each available patch
+ const baseDropDownStub = sinon.stub(element, 'computePatchDropdownContent');
+ element.basePatchNum = 1 as BasePatchSetNum;
+ await element.updateComplete;
+ assert.equal(baseDropDownStub.callCount, 1);
+ });
+
+ test('computePatchDropdownContent', async () => {
+ element.availablePatches = [
+ {num: 'edit', sha: '1'} as PatchSet,
+ {num: 3, sha: '2'} as PatchSet,
+ {num: 2, sha: '3'} as PatchSet,
+ {num: 1, sha: '4'} as PatchSet,
+ ];
+ element.basePatchNum = 1 as BasePatchSetNum;
+ element.revisions = [
+ createRevision(3) as RevisionInfo,
+ createEditRevision(2) as EditRevisionInfo,
+ createRevision(2, 'description') as RevisionInfo,
+ createRevision(1) as RevisionInfo,
+ ];
+ await element.updateComplete;
+
+ const expectedResult: DropdownItem[] = [
+ {
+ disabled: false,
+ triggerText: 'edit',
+ text: 'edit | 1',
+ mobileText: 'edit',
+ bottomText: '',
+ value: 'edit',
+ },
+ {
+ disabled: false,
+ triggerText: 'Patchset 3',
+ text: 'Patchset 3 | 2',
+ mobileText: '3',
+ bottomText: '',
+ value: 3,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ } as DropdownItem,
+ {
+ disabled: false,
+ triggerText: 'Patchset 2',
+ text: 'Patchset 2 | 3',
+ mobileText: '2 description',
+ bottomText: 'description',
+ value: 2,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ } as DropdownItem,
+ {
+ disabled: true,
+ triggerText: 'Patchset 1',
+ text: 'Patchset 1 | 4',
+ mobileText: '1',
+ bottomText: '',
+ value: 1,
+ date: '2020-02-01 01:02:03.000000000' as Timestamp,
+ } as DropdownItem,
+ ];
+
+ assert.deepEqual(element.computePatchDropdownContent(), expectedResult);
+ });
+
+ test('filesWeblinks', async () => {
+ element.filesWeblinks = {
+ meta_a: [
+ {
+ name: 'foo',
+ url: 'f.oo',
+ },
+ ],
+ meta_b: [
+ {
+ name: 'bar',
+ url: 'ba.r',
+ },
+ ],
+ };
+ await element.updateComplete;
+ assert.equal(
+ queryAndAssert(element, 'a[href="f.oo"]').textContent!.trim(),
+ 'foo'
+ );
+ assert.equal(
+ queryAndAssert(element, 'a[href="ba.r"]').textContent!.trim(),
+ 'bar'
+ );
+ });
+
+ test('computePatchSetCommentsString', () => {
+ // Test string with unresolved comments.
+ const comments: PathToCommentsInfoMap = {
+ foo: [
+ {
+ id: '27dcee4d_f7b77cfa' as UrlEncodedCommentId,
+ message: 'test',
+ patch_set: 1 as PatchSetNum,
+ unresolved: true,
+ updated: '2017-10-11 20:48:40.000000000' as Timestamp,
+ },
+ ],
+ bar: [
+ {
+ id: '27dcee4d_f7b77cfa' as UrlEncodedCommentId,
+ message: 'test',
+ patch_set: 1 as PatchSetNum,
+ updated: '2017-10-12 20:48:40.000000000' as Timestamp,
+ },
+ {
+ id: '27dcee4d_f7b77cfa' as UrlEncodedCommentId,
+ message: 'test',
+ patch_set: 1 as PatchSetNum,
+ updated: '2017-10-13 20:48:40.000000000' as Timestamp,
+ },
+ ],
+ abc: [],
+ // Patchset level comment does not contribute to the count
+ [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [
+ {
+ id: '27dcee4d_f7b77cfa' as UrlEncodedCommentId,
+ message: 'test',
+ patch_set: 1 as PatchSetNum,
+ unresolved: true,
+ updated: '2017-10-11 20:48:40.000000000' as Timestamp,
+ },
+ ],
+ };
+ element.changeComments = new ChangeComments(comments);
+
+ assert.equal(
+ element.computePatchSetCommentsString(1 as PatchSetNum),
+ ' (3 comments, 1 unresolved)'
+ );
+
+ // Test string with no unresolved comments.
+ delete comments['foo'];
+ element.changeComments = new ChangeComments(comments);
+ assert.equal(
+ element.computePatchSetCommentsString(1 as PatchSetNum),
+ ' (2 comments)'
+ );
+
+ // Test string with no comments.
+ delete comments['bar'];
+ element.changeComments = new ChangeComments(comments);
+ assert.equal(element.computePatchSetCommentsString(1 as PatchSetNum), '');
+ });
+
+ test('patch-range-change fires', () => {
+ const handler = sinon.stub();
+ element.basePatchNum = 1 as BasePatchSetNum;
+ element.patchNum = 3 as PatchSetNum;
+ element.addEventListener('patch-range-change', handler);
+
+ queryAndAssert<GrDropdownList>(
+ element,
+ '#basePatchDropdown'
+ )._handleValueChange('2', [{text: '', value: '2'}]);
+ assert.isTrue(handler.calledOnce);
+ assert.deepEqual(handler.lastCall.args[0].detail, {
+ basePatchNum: 2,
+ patchNum: 3,
+ });
+
+ // BasePatchNum should not have changed, due to one-way data binding.
+ queryAndAssert<GrDropdownList>(
+ element,
+ '#patchNumDropdown'
+ )._handleValueChange('edit', [{text: '', value: 'edit'}]);
+ assert.deepEqual(handler.lastCall.args[0].detail, {
+ basePatchNum: 1,
+ patchNum: 'edit',
+ });
+ });
+});
diff --git a/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts b/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts
index dcf7236..8ce8ce2 100644
--- a/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts
+++ b/polygerrit-ui/app/elements/diff/gr-range-header/gr-range-header.ts
@@ -55,7 +55,7 @@
override render() {
const icon = this.icon ?? '';
return html` <div class="row">
- <iron-icon class="icon" .icon=${icon}></iron-icon>
+ <iron-icon class="icon" .icon=${icon} aria-hidden="true"></iron-icon>
<slot></slot>
</div>`;
}
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.ts b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.ts
index 551889f..0f64d9e 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.ts
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.ts
@@ -18,7 +18,6 @@
import '../../shared/gr-tooltip/gr-tooltip';
import {GrTooltip} from '../../shared/gr-tooltip/gr-tooltip';
import {customElement, property} from '@polymer/decorators';
-import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-selection-action-box_html';
import {fireEvent} from '../../../utils/event-util';
@@ -56,8 +55,8 @@
this.addEventListener('mousedown', e => this._handleMouseDown(e));
}
- placeAbove(el: Text | Element | Range) {
- flush();
+ async placeAbove(el: Text | Element | Range) {
+ await this.$.tooltip.updateComplete;
const rect = this._getTargetBoundingRect(el);
const boxRect = this.$.tooltip.getBoundingClientRect();
const parentRect = this._getParentBoundingClientRect();
@@ -70,8 +69,8 @@
}px`;
}
- placeBelow(el: Text | Element | Range) {
- flush();
+ async placeBelow(el: Text | Element | Range) {
+ await this.$.tooltip.updateComplete;
const rect = this._getTargetBoundingRect(el);
const boxRect = this.$.tooltip.getBoundingClientRect();
const parentRect = this._getParentBoundingClientRect();
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.js
index 81cf0d6..c978c37 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.js
@@ -83,35 +83,35 @@
{width: 10, height: 10});
});
- test('placeAbove for Element argument', () => {
- element.placeAbove(target);
+ test('placeAbove for Element argument', async () => {
+ await element.placeAbove(target);
assert.equal(element.style.top, '25px');
assert.equal(element.style.left, '72px');
});
- test('placeAbove for Text Node argument', () => {
- element.placeAbove(target.firstChild);
+ test('placeAbove for Text Node argument', async () => {
+ await element.placeAbove(target.firstChild);
assert.equal(element.style.top, '25px');
assert.equal(element.style.left, '72px');
});
- test('placeBelow for Element argument', () => {
- element.placeBelow(target);
+ test('placeBelow for Element argument', async () => {
+ await element.placeBelow(target);
assert.equal(element.style.top, '45px');
assert.equal(element.style.left, '72px');
});
- test('placeBelow for Text Node argument', () => {
- element.placeBelow(target.firstChild);
+ test('placeBelow for Text Node argument', async () => {
+ await element.placeBelow(target.firstChild);
assert.equal(element.style.top, '45px');
assert.equal(element.style.left, '72px');
});
- test('uses document.createRange', () => {
+ test('uses document.createRange', async () => {
sinon.spy(document, 'createRange');
element._getTargetBoundingRect.restore();
sinon.spy(element, '_getTargetBoundingRect');
- element.placeAbove(target.firstChild);
+ await element.placeAbove(target.firstChild);
assert.isTrue(document.createRange.called);
});
});
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
index ad671da..f892410 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
@@ -54,6 +54,7 @@
['text/x-erlang', 'erlang'],
['text/x-fortran', 'fortran'],
['text/x-fsharp', 'fsharp'],
+ ['text/x-gherkin', 'gherkin'],
['text/x-go', 'go'],
['text/x-groovy', 'groovy'],
['text/x-haml', 'haml'],
@@ -558,13 +559,19 @@
*/
_notify(state: SyntaxLayerState) {
if (state.lineNums.left - state.lastNotify.left) {
- this._notifyRange(state.lastNotify.left, state.lineNums.left, Side.LEFT);
+ this._notifyRange(
+ state.lastNotify.left,
+ // We have to notify 1-based inclusive, so subtract 1.
+ state.lineNums.left - 1,
+ Side.LEFT
+ );
state.lastNotify.left = state.lineNums.left;
}
if (state.lineNums.right - state.lastNotify.right) {
this._notifyRange(
state.lastNotify.right,
- state.lineNums.right,
+ // We have to notify 1-based inclusive, so subtract 1.
+ state.lineNums.right - 1,
Side.RIGHT
);
state.lastNotify.right = state.lineNums.right;
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.js
index c907a80..2b03bd3 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.js
@@ -171,6 +171,7 @@
window.hljs = mockHLJS;
const highlightSpy = sinon.spy(mockHLJS, 'highlight');
const processNextSpy = sinon.spy(element, '_processNextLine');
+ const notifyRangeSpy = sinon.spy(element, '_notifyRange');
await element.process();
const linesA = diff.meta_a.lines;
@@ -182,6 +183,11 @@
assert.equal(highlightSpy.callCount, linesA + linesB);
+ assert.isTrue(notifyRangeSpy.called);
+ assert.equal(notifyRangeSpy.lastCall.args[0], 44);
+ assert.equal(notifyRangeSpy.lastCall.args[1], 48);
+ assert.equal(notifyRangeSpy.lastCall.args[2], 'right');
+
// The first line of both sides have a range.
let ranges = [element.baseRanges[0], element.revisionRanges[0]];
for (const range of ranges) {
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
index 3adb0f3..e2acdbf 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.ts
@@ -14,77 +14,117 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/gr-table-styles';
-import '../../../styles/shared-styles';
import '../../shared/gr-list-view/gr-list-view';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-documentation-search_html';
import {getBaseUrl} from '../../../utils/url-util';
-import {customElement, property} from '@polymer/decorators';
import {DocResult} from '../../../types/common';
import {fireTitleChange} from '../../../utils/event-util';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {ListViewParams} from '../../gr-app-types';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {tableStyles} from '../../../styles/gr-table-styles';
+import {LitElement, PropertyValues, html} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
@customElement('gr-documentation-search')
-export class GrDocumentationSearch extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrDocumentationSearch extends LitElement {
/**
* URL params passed from the router.
*/
- @property({type: Object, observer: '_paramsChanged'})
+ @property({type: Object})
params?: ListViewParams;
- @property({type: Array})
- _documentationSearches?: DocResult[];
+ // private but used in test
+ @state() documentationSearches?: DocResult[];
- @property({type: Boolean})
- _loading = true;
+ // private but used in test
+ @state() loading = true;
- @property({type: String})
- _filter?: string;
+ @state() private filter = '';
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
override connectedCallback() {
super.connectedCallback();
fireTitleChange(this, 'Documentation Search');
}
- _paramsChanged(params: ListViewParams) {
- this._loading = true;
- this._filter = params?.filter ?? '';
-
- return this._getDocumentationSearches(this._filter);
+ static override get styles() {
+ return [sharedStyles, tableStyles];
}
- _getDocumentationSearches(filter: string) {
- this._documentationSearches = [];
+ override render() {
+ return html` <gr-list-view
+ .filter=${this.filter}
+ .offset=${0}
+ .loading=${this.loading}
+ .path=${'/Documentation'}
+ >
+ <table id="list" class="genericList">
+ <tbody>
+ <tr class="headerRow">
+ <th class="name topHeader">Name</th>
+ <th class="name topHeader"></th>
+ <th class="name topHeader"></th>
+ </tr>
+ <tr id="loading" class="loadingMsg ${this.loading ? 'loading' : ''}">
+ <td>Loading...</td>
+ </tr>
+ </tbody>
+ <tbody class="${this.loading ? 'loading' : ''}">
+ ${this.documentationSearches?.map(search =>
+ this.renderDocumentationList(search)
+ )}
+ </tbody>
+ </table>
+ </gr-list-view>`;
+ }
+
+ private renderDocumentationList(search: DocResult) {
+ return html`
+ <tr class="table">
+ <td class="name">
+ <a href="${this.computeSearchUrl(search.url)}">${search.title}</a>
+ </td>
+ <td></td>
+ <td></td>
+ </tr>
+ `;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('params')) {
+ this.paramsChanged();
+ }
+ }
+
+ // private but used in test
+ paramsChanged() {
+ this.loading = true;
+ this.filter = this.params?.filter ?? '';
+
+ return this.getDocumentationSearches(this.filter);
+ }
+
+ private getDocumentationSearches(filter: string) {
+ this.documentationSearches = [];
return this.restApiService
.getDocumentationSearches(filter)
.then(searches => {
// Late response.
- if (filter !== this._filter || !searches) {
+ if (filter !== this.filter || !searches) {
return;
}
- this._documentationSearches = searches;
- this._loading = false;
+ this.documentationSearches = searches;
+ })
+ .finally(() => {
+ this.loading = false;
});
}
- _computeSearchUrl(url?: string) {
- if (!url) {
- return '';
- }
+ private computeSearchUrl(url?: string) {
+ if (!url) return '';
return `${getBaseUrl()}/${url}`;
}
-
- computeLoadingClass(loading: boolean) {
- return loading ? 'loading' : '';
- }
}
declare global {
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.ts b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.ts
deleted file mode 100644
index 95ce1ec..0000000
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-table-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <gr-list-view
- filter="[[_filter]]"
- offset="0"
- loading="[[_loading]]"
- path="/Documentation"
- >
- <table id="list" class="genericList">
- <tbody>
- <tr class="headerRow">
- <th class="name topHeader">Name</th>
- <th class="name topHeader"></th>
- <th class="name topHeader"></th>
- </tr>
- <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
- <td>Loading...</td>
- </tr>
- </tbody>
- <tbody class$="[[computeLoadingClass(_loading)]]">
- <template is="dom-repeat" items="[[_documentationSearches]]">
- <tr class="table">
- <td class="name">
- <a href$="[[_computeSearchUrl(item.url)]]">[[item.title]]</a>
- </td>
- <td></td>
- <td></td>
- </tr>
- </template>
- </tbody>
- </table>
- </gr-list-view>
-`;
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.ts b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.ts
index bf6a0d5..4bba9cd 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.ts
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.ts
@@ -19,50 +19,53 @@
import './gr-documentation-search';
import {GrDocumentationSearch} from './gr-documentation-search';
import {page} from '../../../utils/page-wrapper-utils';
-import 'lodash/lodash';
-import {stubRestApi} from '../../../test/test-utils';
+import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
import {DocResult} from '../../../types/common';
-import {ListViewParams} from '../../gr-app-types';
const basicFixture = fixtureFromElement('gr-documentation-search');
-let counter: number;
-const documentationGenerator = () => {
+function documentationGenerator(counter: number) {
return {
- title: `Gerrit Code Review - REST API Developers Notes${++counter}`,
+ title: `Gerrit Code Review - REST API Developers Notes${counter}`,
url: 'Documentation/dev-rest-api.html',
};
-};
+}
+
+function createDocumentationList(n: number) {
+ const list = [];
+ for (let i = 0; i < n; ++i) {
+ list.push(documentationGenerator(i));
+ }
+ return list;
+}
suite('gr-documentation-search tests', () => {
let element: GrDocumentationSearch;
let documentationSearches: DocResult[];
- let value: ListViewParams;
-
- setup(() => {
+ setup(async () => {
sinon.stub(page, 'show');
element = basicFixture.instantiate();
- counter = 0;
+ await element.updateComplete;
});
suite('list with searches for documentation', () => {
setup(async () => {
- documentationSearches = _.times(26, documentationGenerator);
+ documentationSearches = createDocumentationList(26);
stubRestApi('getDocumentationSearches').returns(
Promise.resolve(documentationSearches)
);
- await element._paramsChanged(value);
- await flush();
+ await element.paramsChanged();
+ await element.updateComplete;
});
test('test for test repo in the list', async () => {
assert.equal(
- element._documentationSearches![0].title,
+ element.documentationSearches![1].title,
'Gerrit Code Review - REST API Developers Notes1'
);
assert.equal(
- element._documentationSearches![0].url,
+ element.documentationSearches![1].url,
'Documentation/dev-rest-api.html'
);
});
@@ -70,30 +73,34 @@
suite('filter', () => {
setup(() => {
- documentationSearches = _.times(25, documentationGenerator);
+ documentationSearches = createDocumentationList(25);
});
- test('_paramsChanged', async () => {
+ test('paramsChanged', async () => {
const stub = stubRestApi('getDocumentationSearches').returns(
Promise.resolve(documentationSearches)
);
- const value = {filter: 'test'};
- await element._paramsChanged(value);
+ element.params = {filter: 'test'};
+ await element.paramsChanged();
assert.isTrue(stub.lastCall.calledWithExactly('test'));
});
});
suite('loading', () => {
test('correct contents are displayed', async () => {
- assert.isTrue(element._loading);
- assert.equal(element.computeLoadingClass(element._loading), 'loading');
- assert.equal(getComputedStyle(element.$.loading).display, 'block');
+ assert.isTrue(element.loading);
+ assert.equal(
+ getComputedStyle(queryAndAssert(element, '#loading')).display,
+ 'block'
+ );
- element._loading = false;
+ element.loading = false;
- await flush();
- assert.equal(element.computeLoadingClass(element._loading), '');
- assert.equal(getComputedStyle(element.$.loading).display, 'none');
+ await element.updateComplete;
+ assert.equal(
+ getComputedStyle(queryAndAssert(element, '#loading')).display,
+ 'none'
+ );
});
});
});
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 1615a23..7229b6d 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
@@ -34,9 +34,10 @@
AutocompleteQuery,
AutocompleteSuggestion,
} from '../../shared/gr-autocomplete/gr-autocomplete';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {IronInputElement} from '@polymer/iron-input';
import {fireAlert, fireReload} from '../../../utils/event-util';
+import {assertIsDefined} from '../../../utils/common-util';
export interface GrEditControls {
$: {
@@ -56,10 +57,10 @@
}
@property({type: Object})
- change!: ChangeInfo;
+ change?: ChangeInfo;
@property({type: String})
- patchNum!: PatchSetNum;
+ patchNum?: PatchSetNum;
@property({type: Array})
hiddenActions: string[] = [GrEditConstants.Actions.RESTORE.id];
@@ -76,7 +77,7 @@
@property({type: Object})
_query: AutocompleteQuery;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
@@ -298,6 +299,8 @@
}
_queryFiles(input: string): Promise<AutocompleteSuggestion[]> {
+ assertIsDefined(this.change, 'this.change');
+ assertIsDefined(this.patchNum, 'this.patchNum');
return this.restApiService
.queryChangeFiles(this.change._number, this.patchNum, input)
.then(res => {
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 6198f17..58fa85e 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
@@ -40,6 +40,7 @@
setup(() => {
element = basicFixture.instantiate();
element.change = createChange();
+ element.patchNum = 1 as PatchSetNum;
showDialogSpy = sinon.spy(element, '_showDialog');
closeDialogSpy = sinon.spy(element, '_closeDialog');
hideDialogStub = sinon.stub(element, '_hideAllDialogs');
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts
index 418c368..1b854b4 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.ts
@@ -50,6 +50,7 @@
justify-content: flex-end;
}
gr-dropdown {
+ --gr-dropdown-item-color: var(--link-color);
--gr-button-padding: var(--spacing-xs) var(--spacing-s);
}
#actions {
@@ -69,7 +70,6 @@
--gr-dropdown-item: {
background-color: transparent;
border: none;
- color: var(--link-color);
text-transform: uppercase;
}
}
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 24ebd67..08411c0 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
@@ -29,16 +29,16 @@
import {computeTruncatedPath} from '../../../utils/path-list-util';
import {customElement, observe, property} from '@polymer/decorators';
import {
- ChangeInfo,
PatchSetNum,
EditPreferencesInfo,
Base64FileContent,
NumericChangeId,
EditPatchSetNum,
} from '../../../types/common';
+import {ParsedChangeInfo} from '../../../types/types';
import {HttpMethod, NotifyType} from '../../../constants/constants';
import {fireAlert, fireTitleChange} from '../../../utils/event-util';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {ErrorCallback} from '../../../api/rest';
import {assertIsDefined} from '../../../utils/common-util';
import {debounce, DelayedTask} from '../../../utils/async-util';
@@ -90,7 +90,7 @@
params?: GenerateUrlEditViewParameters;
@property({type: Object, observer: '_editChange'})
- _change?: ChangeInfo | null;
+ _change?: ParsedChangeInfo;
@property({type: Number})
_changeNum?: NumericChangeId;
@@ -128,11 +128,11 @@
@property({type: Number})
_lineNum?: number;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
- private readonly storage = appContext.storageService;
+ private readonly storage = getAppContext().storageService;
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
// Tests use this so needs to be non private
storeTask?: DelayedTask;
@@ -153,13 +153,13 @@
this._prefs = prefs;
});
this.cleanups.push(
- addShortcut(this, {key: 's', modifiers: [Modifier.CTRL_KEY]}, e =>
- this._handleSaveShortcut(e)
+ addShortcut(this, {key: 's', modifiers: [Modifier.CTRL_KEY]}, () =>
+ this._handleSaveShortcut()
)
);
this.cleanups.push(
- addShortcut(this, {key: 's', modifiers: [Modifier.META_KEY]}, e =>
- this._handleSaveShortcut(e)
+ addShortcut(this, {key: 's', modifiers: [Modifier.META_KEY]}, () =>
+ this._handleSaveShortcut()
)
);
}
@@ -211,13 +211,11 @@
return Promise.all(promises);
}
- _getChangeDetail(changeNum: NumericChangeId) {
- return this.restApiService.getDiffChangeDetail(changeNum).then(change => {
- this._change = change;
- });
+ async _getChangeDetail(changeNum: NumericChangeId) {
+ this._change = await this.restApiService.getChangeDetail(changeNum);
}
- _editChange(value?: ChangeInfo | null) {
+ _editChange(value?: ParsedChangeInfo | null) {
if (!value) return;
if (!changeIsMerged(value) && !changeIsAbandoned(value)) return;
fireAlert(
@@ -228,7 +226,7 @@
}
@observe('_change', '_type')
- _editType(change?: ChangeInfo | null, type?: string) {
+ _editType(change?: ParsedChangeInfo | null, type?: string) {
if (!change || !type || !type.startsWith('image/')) return;
// Prevent editing binary files
@@ -259,7 +257,10 @@
_viewEditInChangeView() {
if (this._change)
- GerritNav.navigateToChange(this._change, undefined, undefined, true);
+ GerritNav.navigateToChange(this._change, {
+ isEdit: true,
+ forceReload: true,
+ });
}
_getFileData(
@@ -378,7 +379,7 @@
)
.then(() => {
assertIsDefined(this._change, '_change');
- GerritNav.navigateToChange(this._change);
+ GerritNav.navigateToChange(this._change, {forceReload: true});
});
});
}
@@ -399,8 +400,7 @@
);
}
- _handleSaveShortcut(e: KeyboardEvent) {
- e.preventDefault();
+ _handleSaveShortcut() {
if (!this._saveDisabled) {
this._saveEdit();
}
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 f591ab2..07f3851 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
@@ -16,6 +16,7 @@
*/
import '../../../test/common-test-setup-karma';
+import './gr-editor-view';
import {GrEditorView} from './gr-editor-view';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {HttpMethod} from '../../../constants/constants';
@@ -45,7 +46,7 @@
element = basicFixture.instantiate();
savePathStub = stubRestApi('renameFileInChangeEdit');
saveFileStub = stubRestApi('saveChangeEdit');
- changeDetailStub = stubRestApi('getDiffChangeDetail');
+ changeDetailStub = stubRestApi('getChangeDetail');
navigateStub = sinon.stub(element, '_viewEditInChangeView');
});
@@ -366,8 +367,8 @@
const navStub = sinon.stub(GerritNav, 'navigateToChange');
element._patchNum = EditPatchSetNum;
element._viewEditInChangeView();
- assert.equal(navStub.lastCall.args[1], undefined);
- assert.equal(navStub.lastCall.args[3], true);
+ assert.equal(navStub.lastCall.args[1]!.patchNum, undefined);
+ assert.equal(navStub.lastCall.args[1]!.isEdit, true);
});
suite('keyboard shortcuts', () => {
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index d88eeaf..efcf8f6 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -21,6 +21,7 @@
import './documentation/gr-documentation-search/gr-documentation-search';
import './change-list/gr-change-list-view/gr-change-list-view';
import './change-list/gr-dashboard-view/gr-dashboard-view';
+import './topic/gr-topic-view';
import './change/gr-change-view/gr-change-view';
import './core/gr-error-manager/gr-error-manager';
import './core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog';
@@ -46,7 +47,7 @@
ShortcutListener,
} from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {GerritNav} from './core/gr-navigation/gr-navigation';
-import {appContext} from '../services/app-context';
+import {getAppContext} from '../services/app-context';
import {flush} from '@polymer/polymer/lib/utils/flush';
import {customElement, observe, property} from '@polymer/decorators';
import {GrRouter} from './core/gr-router/gr-router';
@@ -75,13 +76,16 @@
PageErrorEventDetail,
RpcLogEvent,
TitleChangeEventDetail,
+ ValueChangedEvent,
} from '../types/events';
-import {ViewState} from '../types/types';
+import {ChangeListViewState, ViewState} from '../types/types';
import {GerritView} from '../services/router/router-model';
import {LifeCycle} from '../constants/reporting';
import {fireIronAnnounce} from '../utils/event-util';
import {assertIsDefined} from '../utils/common-util';
import {listen} from '../services/shortcuts/shortcuts-service';
+import {resolve, DIPolymerElement} from '../models/dependency';
+import {browserModelToken} from '../models/browser/browser-model';
interface ErrorInfo {
text: string;
@@ -103,7 +107,7 @@
};
// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(PolymerElement);
+const base = KeyboardShortcutMixin(DIPolymerElement);
// TODO(TS): implement AppElement interface from gr-app-types.ts
@customElement('gr-app-element')
@@ -140,6 +144,9 @@
_showDashboardView?: boolean;
@property({type: Boolean})
+ _showTopicView?: boolean;
+
+ @property({type: Boolean})
_showChangeView?: boolean;
@property({type: Boolean})
@@ -208,9 +215,11 @@
@property({type: Boolean})
_mainAriaHidden = false;
- private reporting = appContext.reportingService;
+ private reporting = getAppContext().reportingService;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
+
+ private readonly getBrowserModel = resolve(this, browserModelToken);
override keyboardShortcuts(): ShortcutListener[] {
return [
@@ -229,10 +238,6 @@
constructor() {
super();
- // We just want to instantiate this service somewhere. It is reacting to
- // model changes and updates the config model, but at the moment the service
- // is not called from anywhere.
- appContext.configService;
document.addEventListener(EventType.PAGE_ERROR, e => {
this._handlePageError(e);
});
@@ -254,6 +259,12 @@
document.addEventListener(EventType.GR_RPC_LOG, e => this._handleRpcLog(e));
}
+ override connectedCallback() {
+ super.connectedCallback();
+ const resizeObserver = this.getBrowserModel().observeWidth();
+ resizeObserver.observe(this);
+ }
+
override ready() {
super.ready();
this._updateLoginUrl();
@@ -294,7 +305,6 @@
patchRange: null,
selectedFileIndex: 0,
showReplyDialog: false,
- showDownloadDialog: false,
diffMode: null,
numFilesShown: null,
},
@@ -351,6 +361,7 @@
this.$.errorView.classList.remove('show');
this._showChangeListView = view === GerritView.SEARCH;
this._showDashboardView = view === GerritView.DASHBOARD;
+ this._showTopicView = view === GerritView.TOPIC;
this._showChangeView = view === GerritView.CHANGE;
this._showDiffView = view === GerritView.DIFF;
this._showSettingsView = view === GerritView.SETTINGS;
@@ -608,6 +619,14 @@
? 'app-theme-dark'
: 'app-theme-light';
}
+
+ _handleViewStateChanged(e: ValueChangedEvent<ChangeListViewState>) {
+ if (!this._viewState) return;
+ this._viewState.changeListView = {
+ ...this._viewState.changeListView,
+ ...e.detail.value,
+ };
+ }
}
declare global {
diff --git a/polygerrit-ui/app/elements/gr-app-element_html.ts b/polygerrit-ui/app/elements/gr-app-element_html.ts
index a1e6ac9..fcb3435 100644
--- a/polygerrit-ui/app/elements/gr-app-element_html.ts
+++ b/polygerrit-ui/app/elements/gr-app-element_html.ts
@@ -99,7 +99,7 @@
<gr-endpoint-decorator name="banner"></gr-endpoint-decorator>
<gr-main-header
id="mainHeader"
- search-query="{{params.query}}"
+ search-query="[[params.query]]"
on-mobile-search="_mobileSearchToggle"
on-show-keyboard-shortcuts="handleShowKeyboardShortcuts"
mobile-search-hidden="[[!mobileSearch]]"
@@ -112,7 +112,8 @@
<gr-smart-search
id="search"
label="Search for changes"
- search-query="{{params.query}}"
+ search-query="[[params.query]]"
+ server-config="[[_serverConfig]]"
hidden="[[!mobileSearch]]"
>
</gr-smart-search>
@@ -121,7 +122,8 @@
<gr-change-list-view
params="[[params]]"
account="[[_account]]"
- view-state="{{_viewState.changeListView}}"
+ view-state="[[_viewState.changeListView]]"
+ on-view-state-changed="_handleViewStateChanged"
></gr-change-list-view>
</template>
<template is="dom-if" if="[[_showDashboardView]]" restamp="true">
@@ -131,6 +133,9 @@
view-state="{{_viewState.dashboardView}}"
></gr-dashboard-view>
</template>
+ <template is="dom-if" if="[[_showTopicView]]">
+ <gr-topic-view params="[[params]]"></gr-topic-view>
+ </template>
<!-- Note that the change view does not have restamp="true" set, because we
want to re-use it as long as the change number does not change. -->
<template id="dom-if-change-view" is="dom-if" if="[[_showChangeView]]">
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 de749df..d0525ea 100644
--- a/polygerrit-ui/app/elements/gr-app-global-var-init.ts
+++ b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
@@ -26,10 +26,11 @@
import {page} from '../utils/page-wrapper-utils';
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';
-export function initGlobalVariables() {
+export function initGlobalVariables(appContext: AppContext) {
window.GrAnnotation = GrAnnotation;
window.page = page;
window.GrPluginActionContext = GrPluginActionContext;
- initGerritPluginApi();
+ initGerritPluginApi(appContext);
}
diff --git a/polygerrit-ui/app/elements/gr-app-init.ts b/polygerrit-ui/app/elements/gr-app-init.ts
deleted file mode 100644
index ab38326..0000000
--- a/polygerrit-ui/app/elements/gr-app-init.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {initAppContext} from '../services/app-context-init';
-import {
- initVisibilityReporter,
- initPerformanceReporter,
- initErrorReporter,
-} from '../services/gr-reporting/gr-reporting_impl';
-import {appContext} from '../services/app-context';
-
-initAppContext();
-initVisibilityReporter(appContext);
-initPerformanceReporter(appContext);
-initErrorReporter(appContext);
diff --git a/polygerrit-ui/app/elements/gr-app-types.ts b/polygerrit-ui/app/elements/gr-app-types.ts
index 6c8bdb9..8ff7734 100644
--- a/polygerrit-ui/app/elements/gr-app-types.ts
+++ b/polygerrit-ui/app/elements/gr-app-types.ts
@@ -46,6 +46,11 @@
title?: string;
}
+export interface AppElementTopicParams {
+ view: GerritView.TOPIC;
+ topic?: string;
+}
+
export interface AppElementGroupParams {
view: GerritView.GROUP;
detail?: GroupDetailView;
@@ -124,8 +129,9 @@
edit?: boolean;
patchNum?: RevisionPatchSetNum;
basePatchNum?: BasePatchSetNum;
- queryMap?: Map<string, string> | URLSearchParams;
commentId?: UrlEncodedCommentId;
+ forceReload?: boolean;
+ tab?: string;
}
export interface AppElementJustRegisteredParams {
@@ -140,6 +146,7 @@
export type AppElementParams =
| AppElementDashboardParams
+ | AppElementTopicParams
| AppElementGroupParams
| AppElementAdminParams
| AppElementChangeViewParams
diff --git a/polygerrit-ui/app/elements/gr-app.ts b/polygerrit-ui/app/elements/gr-app.ts
index 463fab9..1d0b1ad 100644
--- a/polygerrit-ui/app/elements/gr-app.ts
+++ b/polygerrit-ui/app/elements/gr-app.ts
@@ -16,7 +16,6 @@
*/
import {safeTypesBridge} from '../utils/safe-types-util';
-import './gr-app-init';
import './font-roboto-local-loader';
// Sets up global Polymer variable, because plugins requires it.
import '../scripts/bundled-polymer';
@@ -36,18 +35,55 @@
import {initGlobalVariables} from './gr-app-global-var-init';
import './gr-app-element';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-app_html';
-import {initGerritPluginApi} from './shared/gr-js-api-interface/gr-gerrit';
-import {customElement} from '@polymer/decorators';
+import {Finalizable} from '../services/registry';
+import {provide} from '../models/dependency';
import {installPolymerResin} from '../scripts/polymer-resin-install';
+import {
+ createAppContext,
+ createAppDependencies,
+} from '../services/app-context-init';
+import {
+ initVisibilityReporter,
+ initPerformanceReporter,
+ initErrorReporter,
+} from '../services/gr-reporting/gr-reporting_impl';
+import {injectAppContext} from '../services/app-context';
+import {html, LitElement} from 'lit';
+import {customElement} from 'lit/decorators';
+
+const appContext = createAppContext();
+injectAppContext(appContext);
+const reportingService = appContext.reportingService;
+initVisibilityReporter(reportingService);
+initPerformanceReporter(reportingService);
+initErrorReporter(reportingService);
+
installPolymerResin(safeTypesBridge);
@customElement('gr-app')
-export class GrApp extends PolymerElement {
- static get template() {
- return htmlTemplate;
+export class GrApp extends LitElement {
+ private finalizables: Finalizable[] = [];
+
+ override connectedCallback() {
+ super.connectedCallback();
+ const dependencies = createAppDependencies(appContext);
+ for (const [token, service] of dependencies) {
+ this.finalizables.push(service);
+ provide(this, token, () => service);
+ }
+ }
+
+ override disconnectedCallback() {
+ for (const f of this.finalizables) {
+ f.finalize();
+ }
+ this.finalizables = [];
+ super.disconnectedCallback();
+ }
+
+ override render() {
+ return html`<gr-app-element id="app-element"></gr-app-element>`;
}
}
@@ -57,5 +93,4 @@
}
}
-initGlobalVariables();
-initGerritPluginApi();
+initGlobalVariables(appContext);
diff --git a/polygerrit-ui/app/elements/gr-app_test.js b/polygerrit-ui/app/elements/gr-app_test.js
deleted file mode 100644
index 5a3b1f2..0000000
--- a/polygerrit-ui/app/elements/gr-app_test.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/**
- * @license
- * 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.
- */
-
-import '../test/common-test-setup-karma.js';
-import './gr-app.js';
-import {appContext} from '../services/app-context.js';
-import {GerritNav} from './core/gr-navigation/gr-navigation.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-import {stubRestApi} from '../test/test-utils.js';
-
-const basicFixture = fixtureFromTemplate(html`<gr-app id="app"></gr-app>`);
-
-suite('gr-app tests', () => {
- let element;
- let configStub;
-
- setup(async () => {
- sinon.stub(appContext.reportingService, 'appStarted');
- stub('gr-account-dropdown', '_getTopContent');
- stub('gr-router', 'start');
- stubRestApi('getAccount').returns(Promise.resolve({}));
- stubRestApi('getAccountCapabilities').returns(Promise.resolve({}));
- configStub = stubRestApi('getConfig').returns(Promise.resolve({
- plugin: {},
- auth: {
- auth_type: undefined,
- },
- }));
- stubRestApi('getPreferences').returns(Promise.resolve({my: []}));
- stubRestApi('getVersion').returns(Promise.resolve(42));
- stubRestApi('probePath').returns(Promise.resolve(42));
-
- element = basicFixture.instantiate();
- await flush();
- });
-
- const appElement = () => element.$['app-element'];
-
- test('reporting', () => {
- assert.isTrue(appElement().reporting.appStarted.calledOnce);
- });
-
- test('reporting called before router start', () => {
- const element = appElement();
- const appStartedStub = element.reporting.appStarted;
- const routerStartStub = element.$.router.start;
- sinon.assert.callOrder(appStartedStub, routerStartStub);
- });
-
- test('passes config to gr-plugin-host', () =>
- configStub.lastCall.returnValue.then(config => {
- assert.deepEqual(appElement().$.plugins.config, config);
- })
- );
-
- test('_paramsChanged sets search page', () => {
- appElement()._paramsChanged({base: {view: GerritNav.View.CHANGE}});
- assert.notOk(appElement()._lastSearchPage);
- appElement()._paramsChanged({base: {view: GerritNav.View.SEARCH}});
- assert.ok(appElement()._lastSearchPage);
- });
-});
-
diff --git a/polygerrit-ui/app/elements/gr-app_test.ts b/polygerrit-ui/app/elements/gr-app_test.ts
new file mode 100644
index 0000000..833f3ac
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-app_test.ts
@@ -0,0 +1,89 @@
+/**
+ * @license
+ * 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.
+ */
+
+import '../test/common-test-setup-karma';
+import './gr-app';
+import {getAppContext} from '../services/app-context';
+import {html} from '@polymer/polymer/lib/utils/html-tag';
+import {queryAndAssert, stubRestApi} from '../test/test-utils';
+import {GrApp} from './gr-app';
+import {
+ createAppElementChangeViewParams,
+ createAppElementSearchViewParams,
+ createPreferences,
+ createServerInfo,
+} from '../test/test-data-generators';
+import {GrAppElement} from './gr-app-element';
+import {GrPluginHost} from './plugins/gr-plugin-host/gr-plugin-host';
+
+const basicFixture = fixtureFromTemplate(html`<gr-app id="app"></gr-app>`);
+
+suite('gr-app tests', () => {
+ let grApp: GrApp;
+ const config = createServerInfo();
+ let appStartedStub: sinon.SinonStub;
+ let routerStartStub: sinon.SinonStub;
+
+ setup(async () => {
+ appStartedStub = sinon.stub(getAppContext().reportingService, 'appStarted');
+ stub('gr-account-dropdown', '_getTopContent');
+ routerStartStub = stub('gr-router', 'start');
+ stubRestApi('getAccount').returns(Promise.resolve(undefined));
+ stubRestApi('getAccountCapabilities').returns(Promise.resolve({}));
+ stubRestApi('getConfig').returns(Promise.resolve(config));
+ stubRestApi('getPreferences').returns(Promise.resolve(createPreferences()));
+ stubRestApi('getVersion').returns(Promise.resolve('42'));
+ stubRestApi('probePath').returns(Promise.resolve(false));
+
+ grApp = basicFixture.instantiate() as GrApp;
+ await flush();
+ });
+
+ test('reporting', () => {
+ assert.isTrue(appStartedStub.calledOnce);
+ });
+
+ test('reporting called before router start', () => {
+ sinon.assert.callOrder(appStartedStub, routerStartStub);
+ });
+
+ test('passes config to gr-plugin-host', () => {
+ const grAppElement = queryAndAssert<GrAppElement>(grApp, '#app-element');
+ const pluginHost = queryAndAssert<GrPluginHost>(grAppElement, '#plugins');
+ assert.deepEqual(pluginHost.config, config);
+ });
+
+ test('_paramsChanged sets search page', () => {
+ const grAppElement = queryAndAssert<GrAppElement>(grApp, '#app-element');
+ const paramsForChangeView = createAppElementChangeViewParams();
+ const paramsForSearchView = createAppElementSearchViewParams();
+
+ grAppElement._paramsChanged({
+ base: paramsForChangeView,
+ value: paramsForChangeView,
+ path: '',
+ });
+ assert.notOk(grAppElement._lastSearchPage);
+
+ grAppElement._paramsChanged({
+ base: paramsForSearchView,
+ value: paramsForSearchView,
+ path: '',
+ });
+ assert.ok(grAppElement._lastSearchPage);
+ });
+});
diff --git a/polygerrit-ui/app/elements/lit/shortcut-controller.ts b/polygerrit-ui/app/elements/lit/shortcut-controller.ts
new file mode 100644
index 0000000..4fcc1d2
--- /dev/null
+++ b/polygerrit-ui/app/elements/lit/shortcut-controller.ts
@@ -0,0 +1,101 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {ReactiveController, ReactiveControllerHost} from 'lit';
+import {Binding} from '../../utils/dom-util';
+import {ShortcutsService} from '../../services/shortcuts/shortcuts-service';
+import {getAppContext} from '../../services/app-context';
+import {Shortcut} from '../../services/shortcuts/shortcuts-config';
+
+interface ShortcutListener {
+ binding: Binding;
+ listener: (e: KeyboardEvent) => void;
+}
+
+interface AbstractListener {
+ shortcut: Shortcut;
+ listener: (e: KeyboardEvent) => void;
+}
+
+type Cleanup = () => void;
+
+export class ShortcutController implements ReactiveController {
+ private readonly service: ShortcutsService = getAppContext().shortcutsService;
+
+ private readonly listenersLocal: ShortcutListener[] = [];
+
+ private readonly listenersGlobal: ShortcutListener[] = [];
+
+ private readonly listenersAbstract: AbstractListener[] = [];
+
+ private cleanups: Cleanup[] = [];
+
+ constructor(private readonly host: ReactiveControllerHost & HTMLElement) {
+ host.addController(this);
+ }
+
+ // Note that local shortcuts are *not* suppressed when the user has shortcuts
+ // disabled or when the event comes from elements like <input>. So this method
+ // is intended for shortcuts like ESC and Ctrl-ENTER.
+ // If you need suppressed local shortcuts, then just add an options parameter.
+ addLocal(binding: Binding, listener: (e: KeyboardEvent) => void) {
+ this.listenersLocal.push({binding, listener});
+ }
+
+ addGlobal(binding: Binding, listener: (e: KeyboardEvent) => void) {
+ this.listenersGlobal.push({binding, listener});
+ }
+
+ /**
+ * `Shortcut` is more abstract than a concrete `Binding`. A `Shortcut` has a
+ * description text and (several) bindings configured in the file
+ * `shortcuts-config.ts`.
+ *
+ * Use this method when you are migrating from Polymer to Lit. Call it for
+ * each entry of keyboardShortcuts().
+ */
+ addAbstract(shortcut: Shortcut, listener: (e: KeyboardEvent) => void) {
+ this.listenersAbstract.push({shortcut, listener});
+ }
+
+ hostConnected() {
+ for (const {binding, listener} of this.listenersLocal) {
+ const cleanup = this.service.addShortcut(this.host, binding, listener, {
+ shouldSuppress: false,
+ });
+ this.cleanups.push(cleanup);
+ }
+ for (const {shortcut, listener} of this.listenersAbstract) {
+ const cleanup = this.service.addShortcutListener(shortcut, listener);
+ this.cleanups.push(cleanup);
+ }
+ for (const {binding, listener} of this.listenersGlobal) {
+ const cleanup = this.service.addShortcut(
+ document.body,
+ binding,
+ listener
+ );
+ this.cleanups.push(cleanup);
+ }
+ }
+
+ hostDisconnected() {
+ for (const cleanup of this.cleanups) {
+ cleanup();
+ }
+ this.cleanups = [];
+ }
+}
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 7a91c68..cf0e23a 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
@@ -16,7 +16,7 @@
*/
import {EventType, PluginApi} from '../../../api/plugin';
import {AdminPluginApi, MenuLink} from '../../../api/admin';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
/**
* GrAdminApi class.
@@ -27,7 +27,7 @@
// TODO(TS): maybe define as enum if its a limited set
private menuLinks: MenuLink[] = [];
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
constructor(private readonly plugin: PluginApi) {
this.reporting.trackApi(this.plugin, 'admin', 'constructor');
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.js b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.js
index 9a8f75e..6fd2505 100644
--- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.js
@@ -18,16 +18,13 @@
import '../../../test/common-test-setup-karma.js';
import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
-
-const pluginApi = _testOnly_initGerritPluginApi();
suite('gr-admin-api tests', () => {
let adminApi;
setup(() => {
let plugin;
- pluginApi.install(p => { plugin = p; }, '0.1',
+ window.Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
getPluginLoader().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 c3d9e4d..0654914 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
@@ -16,13 +16,13 @@
*/
import {AttributeHelperPluginApi} from '../../../api/attribute-helper';
import {PluginApi} from '../../../api/plugin';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
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 = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
// TODO(TS): Change any to something more like HTMLElement.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.js b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.js
index 2d83012..94eb292 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.js
@@ -17,7 +17,6 @@
import '../../../test/common-test-setup-karma.js';
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
Polymer({
is: 'gr-attribute-helper-some-element',
@@ -31,15 +30,13 @@
const basicFixture = fixtureFromElement('gr-attribute-helper-some-element');
-const pluginApi = _testOnly_initGerritPluginApi();
-
suite('gr-attribute-helper tests', () => {
let element;
let instance;
setup(() => {
let plugin;
- pluginApi.install(p => { plugin = p; }, '0.1',
+ window.Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
element = basicFixture.instantiate();
instance = plugin.attributeHelper(element);
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 087779e..e1f3d3c 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
@@ -22,7 +22,7 @@
CheckResult,
CheckRun,
} from '../../../api/checks';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
const DEFAULT_CONFIG: ChecksApiConfig = {
fetchPollingIntervalSeconds: 60,
@@ -43,9 +43,9 @@
export class GrChecksApi implements ChecksPluginApi {
private state = State.NOT_REGISTERED;
- private readonly checksService = appContext.checksService;
+ private readonly checksModel = getAppContext().checksModel;
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
constructor(readonly plugin: PluginApi) {
this.reporting.trackApi(this.plugin, 'checks', 'constructor');
@@ -53,14 +53,14 @@
announceUpdate() {
this.reporting.trackApi(this.plugin, 'checks', 'announceUpdate');
- this.checksService.reload(this.plugin.getPluginName());
+ this.checksModel.reload(this.plugin.getPluginName());
}
updateResult(run: CheckRun, result: CheckResult) {
if (result.externalId === undefined) {
throw new Error('ChecksApi.updateResult() was called without externalId');
}
- this.checksService.updateResult(this.plugin.getPluginName(), run, result);
+ this.checksModel.updateResult(this.plugin.getPluginName(), run, result);
}
register(provider: ChecksProvider, config?: ChecksApiConfig): void {
@@ -68,7 +68,7 @@
if (this.state === State.REGISTERED)
throw new Error('Only one provider can be registered per plugin.');
this.state = State.REGISTERED;
- this.checksService.register(
+ this.checksModel.register(
this.plugin.getPluginName(),
provider,
config ?? DEFAULT_CONFIG
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 e1ec158..596c54b 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
@@ -17,18 +17,15 @@
import '../../../test/common-test-setup-karma';
import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit';
import {PluginApi} from '../../../api/plugin';
import {ChecksPluginApi} from '../../../api/checks';
-const gerritPluginApi = _testOnly_initGerritPluginApi();
-
suite('gr-settings-api tests', () => {
let checksApi: ChecksPluginApi | undefined;
setup(() => {
let pluginApi: PluginApi | undefined = undefined;
- gerritPluginApi.install(
+ window.Gerrit.install(
p => {
pluginApi = p;
},
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.js b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.js
index 883f2a6..025f2b4 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.js
@@ -18,9 +18,6 @@
import '../../../test/common-test-setup-karma.js';
import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
import {GrDomHook, GrDomHooksManager} from './gr-dom-hooks.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
-
-const pluginApi = _testOnly_initGerritPluginApi();
suite('gr-dom-hooks tests', () => {
let instance;
@@ -28,7 +25,7 @@
setup(() => {
let plugin;
- pluginApi.install(p => { plugin = p; }, '0.1',
+ window.Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
instance = new GrDomHooksManager(plugin);
});
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.js
index 1be5e82..893f0d1 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.js
@@ -21,9 +21,6 @@
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
import {resetPlugins} from '../../../test/test-utils.js';
import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
-
-const pluginApi = _testOnly_initGerritPluginApi();
const basicFixture = fixtureFromTemplate(
html`<div>
@@ -54,7 +51,9 @@
setup(async () => {
resetPlugins();
container = basicFixture.instantiate();
- pluginApi.install(p => plugin = p, '0.1',
+ window.Gerrit.install(
+ p => { plugin = p; },
+ '0.1',
'http://some/plugin/url.js');
// Decoration
decorationHook = plugin.registerCustomComponent('first', 'some-module');
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-slot/gr-endpoint-slot.ts b/polygerrit-ui/app/elements/plugins/gr-endpoint-slot/gr-endpoint-slot.ts
index 4999716..f15b046 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-slot/gr-endpoint-slot.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-slot/gr-endpoint-slot.ts
@@ -17,6 +17,12 @@
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {customElement, property} from '@polymer/decorators';
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-endpoint-slot': GrEndpointSlot;
+ }
+}
+
/**
* `gr-endpoint-slot` is used when need control over where
* the registered element should appear inside of the endpoint.
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 0c36cd5..24fc613 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
@@ -19,10 +19,10 @@
UnsubscribeCallback,
} from '../../../api/event-helper';
import {PluginApi} from '../../../api/plugin';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
export class GrEventHelper implements EventHelperPluginApi {
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
constructor(readonly plugin: PluginApi, readonly element: HTMLElement) {
this.reporting.trackApi(this.plugin, 'event', 'constructor');
@@ -56,8 +56,12 @@
let mayContinue = true;
try {
mayContinue = callback(e);
- } catch (exception) {
- this.reporting.error(exception);
+ } catch (exception: unknown) {
+ this.reporting.error(
+ new Error('event listener callback error'),
+ undefined,
+ exception
+ );
}
if (mayContinue === false) {
e.stopImmediatePropagation();
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.js b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.js
index 4e3d657..13bd535 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.js
@@ -18,7 +18,6 @@
import '../../../test/common-test-setup-karma.js';
import {addListener} from '@polymer/polymer/lib/utils/gestures.js';
import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
import {mockPromise} from '../../../test/test-utils.js';
Polymer({
@@ -34,15 +33,13 @@
const basicFixture = fixtureFromElement('gr-event-helper-some-element');
-const pluginApi = _testOnly_initGerritPluginApi();
-
suite('gr-event-helper tests', () => {
let element;
let instance;
setup(() => {
let plugin;
- pluginApi.install(p => { plugin = p; }, '0.1',
+ window.Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
element = basicFixture.instantiate();
instance = plugin.eventHelper(element);
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.js
index a192f80..faf7525 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.js
@@ -18,11 +18,8 @@
import {resetPlugins} from '../../../test/test-utils.js';
import './gr-external-style.js';
import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-const pluginApi = _testOnly_initGerritPluginApi();
-
const basicFixture = fixtureFromTemplate(
html`<gr-external-style name="foo"></gr-external-style>`
);
@@ -35,7 +32,7 @@
const installPlugin = () => {
if (plugin) { return; }
- pluginApi.install(p => {
+ window.Gerrit.install(p => {
plugin = p;
}, '0.1', TEST_URL);
};
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.ts b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.ts
index 45a93bf..53f7095 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.ts
@@ -19,7 +19,7 @@
import {GrPluginPopup} from './gr-plugin-popup';
import {PluginApi} from '../../../api/plugin';
import {PopupPluginApi} from '../../../api/popup';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
interface CustomPolymerPluginEl extends HTMLElement {
plugin: PluginApi;
@@ -36,7 +36,7 @@
private popup: GrPluginPopup | null = null;
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
constructor(
readonly plugin: PluginApi,
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.js
index 2889333..beedfab 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.js
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.js
@@ -18,7 +18,6 @@
import '../../../test/common-test-setup-karma.js';
import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
import {GrPopupInterface} from './gr-popup-interface.js';
-import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
@@ -34,14 +33,13 @@
const containerFixture = fixtureFromElement('div');
-const pluginApi = _testOnly_initGerritPluginApi();
suite('gr-popup-interface tests', () => {
let container;
let instance;
let plugin;
setup(() => {
- pluginApi.install(p => { plugin = p; }, '0.1',
+ window.Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
container = containerFixture.instantiate();
sinon.stub(plugin, 'hook').returns({
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 bd6835c..293fd14 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
@@ -24,7 +24,7 @@
import {customElement, property, observe} from '@polymer/decorators';
import {AccountDetailInfo, ServerInfo} from '../../../types/common';
import {EditableAccountField} from '../../../constants/constants';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {fireEvent} from '../../../utils/event-util';
@customElement('gr-account-info')
@@ -92,7 +92,7 @@
@property({type: String})
_avatarChangeUrl = '';
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
loadData() {
const promises = [];
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 f1813a4..282aa11 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts
@@ -69,6 +69,72 @@
await flush();
});
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(`<div class="gr-form-styles">
+ <section>
+ <span class="title"></span>
+ <span class="value">
+ <gr-avatar hidden="" imagesize="120"></gr-avatar>
+ </span>
+ </section>
+ <section class="hide">
+ <span class="title"></span>
+ <span class="value"><a href="">Change avatar</a></span>
+ </section>
+ <section>
+ <span class="title">ID</span>
+ <span class="value">123</span>
+ </section>
+ <section>
+ <span class="title">Email</span>
+ <span class="value">user-123@</span>
+ </section>
+ <section>
+ <span class="title">Registered</span>
+ <span class="value">
+ <gr-date-formatter withtooltip=""></gr-date-formatter>
+ </span>
+ </section>
+ <section id="usernameSection">
+ <span class="title">Username</span>
+ <span class="value"></span>
+ <span class="value" hidden="true">
+ <iron-input id="usernameIronInput">
+ <input id="usernameInput">
+ </iron-input>
+ </span>
+ </section>
+ <section id="nameSection">
+ <label class="title" for="nameInput">Full name</label>
+ <span class="value">User-123</span>
+ <span class="value" hidden="true">
+ <iron-input id="nameIronInput">
+ <input id="nameInput">
+ </iron-input>
+ </span>
+ </section>
+ <section>
+ <label class="title" for="displayNameInput">Display name</label>
+ <span class="value">
+ <iron-input>
+ <input id="displayNameInput">
+ </iron-input>
+ </span>
+ </section>
+ <section>
+ <label class="title" for="statusInput">
+ Status (e.g. "Vacation")
+ </label>
+ <span class="value">
+ <iron-input>
+ <input id="statusInput">
+ </iron-input>
+ </span>
+ </section>
+ </div>
+ `);
+ });
+
test('basic account info render', () => {
assert.isFalse(element._loading);
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.ts b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.ts
index a972db3..43aefdc 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.ts
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.ts
@@ -17,7 +17,7 @@
import {getBaseUrl} from '../../../utils/url-util';
import {ContributorAgreementInfo} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {formStyles} from '../../../styles/gr-form-styles';
import {sharedStyles} from '../../../styles/shared-styles';
import {css, html, LitElement} from 'lit';
@@ -28,7 +28,7 @@
@property({type: Array})
_agreements?: ContributorAgreementInfo[];
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
override connectedCallback() {
super.connectedCallback();
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
index 96b1ded..0a9fbbf 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.ts
@@ -22,8 +22,9 @@
import {htmlTemplate} from './gr-change-table-editor_html';
import {customElement, property, observe} from '@polymer/decorators';
import {ServerInfo} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {columnNames} from '../../change-list/gr-change-list/gr-change-list';
+import {KnownExperimentId} from '../../../services/flags/flags';
@customElement('gr-change-table-editor')
export class GrChangeTableEditor extends PolymerElement {
@@ -43,33 +44,30 @@
@property({type: Array})
defaultColumns: string[] = [];
- private readonly flagsService = appContext.flagsService;
+ private readonly flagsService = getAppContext().flagsService;
@observe('serverConfig')
_configChanged(config: ServerInfo) {
this.defaultColumns = columnNames.filter(col =>
- this._isColumnEnabled(col, config, this.flagsService.enabledExperiments)
+ this._isColumnEnabled(col, config)
);
if (!this.displayedColumns) return;
this.displayedColumns = this.displayedColumns.filter(column =>
- this._isColumnEnabled(
- column,
- config,
- this.flagsService.enabledExperiments
- )
+ this._isColumnEnabled(column, config)
);
}
/**
- * Is the column disabled by a server config or experiment? For example the
- * assignee feature might be disabled and thus the corresponding column is
- * also disabled.
- *
+ * Is the column disabled by a server config or experiment?
*/
- _isColumnEnabled(column: string, config: ServerInfo, experiments: string[]) {
+ _isColumnEnabled(column: string, config: ServerInfo) {
if (!config || !config.change) return true;
- if (column === 'Assignee') return !!config.change.enable_assignee;
- if (column === 'Comments') return experiments.includes('comments-column');
+ if (column === 'Comments')
+ return this.flagsService.isEnabled('comments-column');
+ if (column === 'Requirements')
+ return this.flagsService.isEnabled(
+ KnownExperimentId.SUBMIT_REQUIREMENTS_UI
+ );
return true;
}
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
index 4f8d0a0..c2bcec2 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.ts
@@ -20,7 +20,6 @@
import {GrChangeTableEditor} from './gr-change-table-editor';
import {queryAndAssert} from '../../../test/test-utils';
import {createServerInfo} from '../../../test/test-data-generators';
-import {ServerInfo} from '../../../types/common';
const basicFixture = fixtureFromElement('gr-change-table-editor');
@@ -35,7 +34,6 @@
'Subject',
'Status',
'Owner',
- 'Assignee',
'Reviewers',
'Comments',
'Repo',
@@ -62,16 +60,6 @@
}
});
- test('disabled experiments are hidden', () => {
- assert.isFalse(element.displayedColumns.includes('Assignee'));
- element.set('displayedColumns', columns);
- const config: ServerInfo = {...createServerInfo()};
- config.change.enable_assignee = true;
- element.serverConfig = config;
- flush();
- assert.isTrue(element.displayedColumns.includes('Assignee'));
- });
-
test('hide item', () => {
const checkbox = queryAndAssert<HTMLInputElement>(
element,
@@ -91,7 +79,6 @@
element.set('displayedColumns', [
'Status',
'Owner',
- 'Assignee',
'Repo',
'Branch',
'Updated',
@@ -117,7 +104,7 @@
test('_getDisplayedColumns', () => {
const enabledColumns = columns.filter(column =>
- element._isColumnEnabled(column, element.serverConfig!, [])
+ element._isColumnEnabled(column, element.serverConfig!)
);
assert.deepEqual(element._getDisplayedColumns(), enabledColumns);
const input = queryAndAssert<HTMLInputElement>(
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.ts b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.ts
index 5b757e6..92d984d 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.ts
@@ -30,7 +30,7 @@
ContributorAgreementInfo,
} from '../../../types/common';
import {fireAlert, fireTitleChange} from '../../../utils/event-util';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
declare global {
interface HTMLElementTagNameMap {
@@ -65,7 +65,7 @@
@property({type: String})
_agreementsUrl?: string;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
override connectedCallback() {
super.connectedCallback();
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 7cfd1b3..5cd1acb 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
@@ -22,7 +22,7 @@
import {htmlTemplate} from './gr-edit-preferences_html';
import {customElement, property} from '@polymer/decorators';
import {EditPreferencesInfo} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
export interface GrEditPreferences {
$: {
@@ -50,7 +50,7 @@
@property({type: Object})
editPrefs?: EditPreferencesInfo;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
loadData() {
return this.restApiService.getEditPreferences().then(prefs => {
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.ts b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.ts
index 7f25a86..afd04b4 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.ts
@@ -22,7 +22,7 @@
import {htmlTemplate} from './gr-email-editor_html';
import {customElement, property} from '@polymer/decorators';
import {EmailInfo} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
@customElement('gr-email-editor')
export class GrEmailEditor extends PolymerElement {
@@ -42,7 +42,7 @@
@property({type: String})
_newPreferred: string | null = null;
- readonly restApiService = appContext.restApiService;
+ readonly restApiService = getAppContext().restApiService;
loadData() {
return this.restApiService.getAccountEmails().then(emails => {
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 afbd67e..ed07fd6 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
@@ -28,7 +28,7 @@
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
export interface GrGpgEditor {
$: {
@@ -64,7 +64,7 @@
@property({type: Array})
_keysToRemove: GpgKeyInfo[] = [];
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
loadData() {
this._keys = [];
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.ts b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.ts
index 8f1706d..2db8752 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.ts
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.ts
@@ -17,7 +17,7 @@
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {GroupInfo, GroupId} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
+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';
@@ -33,7 +33,7 @@
@state()
protected _groups: GroupInfo[] = [];
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
loadData() {
return this.restApiService.getAccountGroups().then(groups => {
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 59f6a39..ebe30a3 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
@@ -18,7 +18,7 @@
import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
import '../../shared/gr-overlay/gr-overlay';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
-import {appContext} from '../../../services/app-context';
+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';
@@ -44,7 +44,7 @@
@property({type: String})
_passwordUrl: string | null = null;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
override connectedCallback() {
super.connectedCallback();
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 d65304c..9e1aaa9 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.ts
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.ts
@@ -26,9 +26,10 @@
import {AccountExternalIdInfo, ServerInfo} from '../../../types/common';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {PolymerDomRepeatEvent} from '../../../types/types';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
+import {AuthType} from '../../../constants/constants';
-const AUTH = ['OPENID', 'OAUTH'];
+const AUTH = [AuthType.OPENID, AuthType.OAUTH];
export interface GrIdentities {
$: {
@@ -57,7 +58,7 @@
})
_showLinkAnotherIdentity?: boolean;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
loadData() {
return this.restApiService.getExternalIds().then(id => {
@@ -104,8 +105,8 @@
}
_computeShowLinkAnotherIdentity(config?: ServerInfo) {
- if (config?.auth?.git_basic_auth_policy) {
- return AUTH.includes(config.auth.git_basic_auth_policy.toUpperCase());
+ if (config?.auth?.auth_type) {
+ return AUTH.includes(config.auth.auth_type);
}
return false;
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 9d8dcc5..ecc322ef7 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
@@ -18,6 +18,7 @@
import '../../../test/common-test-setup-karma';
import './gr-identities';
import {GrIdentities} from './gr-identities';
+import {AuthType} from '../../../constants/constants';
import {stubRestApi} from '../../../test/test-utils';
import {ServerInfo} from '../../../types/common';
import {createServerInfo} from '../../../test/test-data-generators';
@@ -107,19 +108,19 @@
...createServerInfo(),
};
- config.auth.git_basic_auth_policy = 'OAUTH';
+ config.auth.auth_type = AuthType.OAUTH;
assert.isTrue(element._computeShowLinkAnotherIdentity(config));
- config.auth.git_basic_auth_policy = 'OpenID';
+ config.auth.auth_type = AuthType.OPENID;
assert.isTrue(element._computeShowLinkAnotherIdentity(config));
- config.auth.git_basic_auth_policy = 'HTTP_LDAP';
+ config.auth.auth_type = AuthType.HTTP_LDAP;
assert.isFalse(element._computeShowLinkAnotherIdentity(config));
- config.auth.git_basic_auth_policy = 'LDAP';
+ config.auth.auth_type = AuthType.LDAP;
assert.isFalse(element._computeShowLinkAnotherIdentity(config));
- config.auth.git_basic_auth_policy = 'HTTP';
+ config.auth.auth_type = AuthType.HTTP;
assert.isFalse(element._computeShowLinkAnotherIdentity(config));
assert.isFalse(element._computeShowLinkAnotherIdentity(undefined));
@@ -129,7 +130,7 @@
let config: ServerInfo = {
...createServerInfo(),
};
- config.auth.git_basic_auth_policy = 'OAUTH';
+ config.auth.auth_type = AuthType.OAUTH;
element.serverConfig = config;
@@ -138,7 +139,7 @@
config = {
...createServerInfo(),
};
- config.auth.git_basic_auth_policy = 'LDAP';
+ config.auth.auth_type = AuthType.LDAP;
element.serverConfig = config;
assert.isFalse(element._showLinkAnotherIdentity);
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.ts b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.ts
index aa8f62b..67ff0c4 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.ts
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.ts
@@ -23,13 +23,14 @@
import {customElement, property, observe} from '@polymer/decorators';
import {ServerInfo, AccountDetailInfo} from '../../../types/common';
import {EditableAccountField} from '../../../constants/constants';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {fireEvent} from '../../../utils/event-util';
export interface GrRegistrationDialog {
$: {
name: HTMLInputElement;
username: HTMLInputElement;
+ displayName: HTMLInputElement;
};
}
@@ -83,7 +84,20 @@
@property({type: String, observer: '_usernameChanged'})
_username?: string;
- private readonly restApiService = appContext.restApiService;
+ @property({
+ type: Boolean,
+ notify: true,
+ computed: '_computeNameMutable(_serverConfig)',
+ })
+ _nameMutable?: boolean;
+
+ @property({type: Boolean})
+ _hasNameChange?: boolean;
+
+ @property({type: Boolean})
+ _hasDisplayNameChange?: boolean;
+
+ private readonly restApiService = getAppContext().restApiService;
override ready() {
super.ready();
@@ -95,10 +109,13 @@
const loadAccount = this.restApiService.getAccount().then(account => {
if (!account) return;
+ this._hasNameChange = false;
this._hasUsernameChange = false;
+ this._hasDisplayNameChange = false;
// Provide predefined value for username to trigger computation of
// username mutability.
account.username = account.username || '';
+
this._account = account;
this._username = account.username;
});
@@ -120,6 +137,22 @@
(this._account.username || '') !== (this._username || '');
}
+ @observe('_account.display_name')
+ _displayNameChanged() {
+ if (this._loading || !this._account) {
+ return;
+ }
+ this._hasDisplayNameChange = true;
+ }
+
+ @observe('_account.name')
+ _nameChanged() {
+ if (this._loading || !this._account) {
+ return;
+ }
+ this._hasNameChange = true;
+ }
+
_computeUsernameMutable(username?: string) {
// Username may not be changed once it is set.
return !username;
@@ -131,17 +164,32 @@
);
}
+ _computeNameMutable(config: ServerInfo) {
+ return config.auth.editable_account_fields.includes(
+ EditableAccountField.FULL_NAME
+ );
+ }
+
_save() {
this._saving = true;
- const promises = [this.restApiService.setAccountName(this.$.name.value)];
-
+ const promises = [];
// Note that we are intentionally not acting on this._username being the
// empty string (which is falsy).
if (this._hasUsernameChange && this._usernameMutable && this._username) {
promises.push(this.restApiService.setAccountUsername(this._username));
}
+ if (this._hasNameChange && this._nameMutable && this._account?.name) {
+ promises.push(this.restApiService.setAccountName(this._account.name));
+ }
+
+ if (this._hasDisplayNameChange && this._account?.display_name) {
+ promises.push(
+ this.restApiService.setAccountDisplayName(this._account.display_name)
+ );
+ }
+
return Promise.all(promises).then(() => {
this._saving = false;
fireEvent(this, 'account-detail-update');
@@ -163,8 +211,13 @@
fireEvent(this, 'close');
}
- _computeSaveDisabled(name?: string, username?: string, saving?: boolean) {
- return saving || (!name && !username);
+ _computeSaveDisabled(
+ displayName?: string,
+ name?: string,
+ username?: string,
+ saving?: boolean
+ ) {
+ return saving || (!displayName && !name && !username);
}
@observe('_loading')
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.ts b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.ts
index 6f270f5..4484631 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.ts
@@ -73,12 +73,21 @@
<hr />
<section>
<span class="title">Full Name</span>
- <span class="value">
+ <span hidden$="[[_nameMutable]]" class="value">[[_account.name]]</span>
+ <span hidden$="[[!_nameMutable]]" class="value">
<iron-input bind-value="{{_account.name}}">
<input id="name" disabled="[[_saving]]" />
</iron-input>
</span>
</section>
+ <section>
+ <span class="title">Display Name</span>
+ <span class="value">
+ <iron-input bind-value="{{_account.display_name}}">
+ <input id="displayName" disabled="[[_saving]]" />
+ </iron-input>
+ </span>
+ </section>
<template is="dom-if" if="[[_computeUsernameEditable(_serverConfig)]]">
<section>
<span class="title">Username</span>
@@ -110,7 +119,7 @@
id="saveButton"
primary=""
link=""
- disabled="[[_computeSaveDisabled(_account.name, _username, _saving)]]"
+ disabled="[[_computeSaveDisabled(_account.display_name, _account.name, _username, _saving)]]"
on-click="_handleSave"
>Save</gr-button
>
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.ts b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.ts
index 3c09d5e..78b7d60 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.ts
@@ -36,10 +36,15 @@
account = {
name: 'name',
+ display_name: 'display name',
registered_on: '2018-02-08 18:49:18.000000000' as Timestamp,
};
- stubRestApi('getAccount').returns(Promise.resolve(account));
+ stubRestApi('getAccount').returns(
+ Promise.resolve({
+ ...account,
+ })
+ );
stubRestApi('setAccountName').callsFake(name => {
account.name = name;
return Promise.resolve();
@@ -48,12 +53,19 @@
account.username = username;
return Promise.resolve();
});
+ stubRestApi('setAccountDisplayName').callsFake(displayName => {
+ account.display_name = displayName;
+ return Promise.resolve();
+ });
stubRestApi('getConfig').returns(
Promise.resolve({
...createServerInfo(),
auth: {
auth_type: AuthType.HTTP,
- editable_account_fields: [EditableAccountField.USER_NAME],
+ editable_account_fields: [
+ EditableAccountField.USER_NAME,
+ EditableAccountField.FULL_NAME,
+ ],
},
})
);
@@ -106,31 +118,35 @@
test('saves account details', async () => {
await flush();
- element.$.name.value = 'new name';
element.set('_account.username', '');
element._hasUsernameChange = false;
assert.isTrue(element._usernameMutable);
element.set('_username', 'new username');
+ element.set('_account.name', 'new name');
+ element.set('_account.display_name', 'new display name');
// Nothing should be committed yet.
assert.equal(account.name, 'name');
assert.isNotOk(account.username);
+ assert.equal(account.display_name, 'display name');
// Save and verify new values are committed.
await save();
assert.equal(account.name, 'new name');
assert.equal(account.username, 'new username');
+ assert.equal(account.display_name, 'new display name');
});
test('save btn disabled', () => {
const compute = element._computeSaveDisabled;
- assert.isTrue(compute('', '', false));
- assert.isFalse(compute('', 'test', false));
- assert.isFalse(compute('test', '', false));
- assert.isTrue(compute('test', 'test', true));
- assert.isFalse(compute('test', 'test', false));
+ assert.isTrue(compute('', '', '', false));
+ assert.isFalse(compute('', '', 'test', false));
+ assert.isFalse(compute('', 'test', '', false));
+ assert.isFalse(compute('test', '', '', false));
+ assert.isTrue(compute('test', 'test', 'test', true));
+ assert.isFalse(compute('test', 'test', 'test', false));
});
test('_computeUsernameMutable', () => {
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 127ac69..2da3765 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
@@ -20,6 +20,7 @@
import '../../../styles/gr-form-styles';
import '../../../styles/gr-menu-page-styles';
import '../../../styles/gr-page-nav-styles';
+import '../../../styles/gr-paper-styles';
import '../../../styles/shared-styles';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../gr-change-table-editor/gr-change-table-editor';
@@ -59,7 +60,7 @@
import {GrGpgEditor} from '../gr-gpg-editor/gr-gpg-editor';
import {GrEmailEditor} from '../gr-email-editor/gr-email-editor';
import {fireAlert, fireTitleChange} from '../../../utils/event-util';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {GerritView} from '../../../services/router/router-model';
import {
DateFormat,
@@ -223,7 +224,7 @@
public _testOnly_loadingPromise?: Promise<void>;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
override connectedCallback() {
super.connectedCallback();
@@ -240,7 +241,6 @@
this.$.groupList.loadData(),
this.$.identities.loadData(),
this.$.editPrefs.loadData(),
- this.$.diffPrefs.loadData(),
];
// TODO(dhruvsri): move this to the service
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
index 81e4fc4..c1ebcac 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.ts
@@ -20,6 +20,9 @@
<style include="gr-font-styles">
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
+ <style include="gr-paper-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="shared-styles">
:host {
color: var(--primary-text-color);
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 c1f347f..95d15b8 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
@@ -28,7 +28,7 @@
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 {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
export interface GrSshEditor {
$: {
@@ -64,7 +64,7 @@
@property({type: Array})
_keysToRemove: SshKeyInfo[] = [];
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
loadData() {
return this.restApiService.getAccountSSHKeys().then(keys => {
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 4381a59..32ca2c5 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
@@ -30,7 +30,7 @@
} from '../../shared/gr-autocomplete/gr-autocomplete';
import {hasOwnProperty} from '../../../utils/common-util';
import {ProjectWatchInfo} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {IronInputElement} from '@polymer/iron-input';
const NOTIFICATION_TYPES = [
@@ -67,7 +67,7 @@
@property({type: Object})
_query: AutocompleteQuery;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
index 31c62b1..9ce62c1 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.ts
@@ -18,7 +18,7 @@
import '../gr-button/gr-button';
import '../gr-icons/gr-icons';
import {AccountInfo, ChangeInfo} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators';
import {classMap} from 'lit/directives/class-map';
@@ -79,7 +79,7 @@
@property({type: Boolean})
transparentBackground = false;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
static override get styles() {
return [
@@ -96,6 +96,7 @@
border: 1px solid var(--border-color);
display: inline-flex;
padding: 0 1px;
+ --account-label-padding-horizontal: 6px;
}
:host:focus {
border-color: transparent;
@@ -131,9 +132,6 @@
/* eslint-disable lit/prefer-static-styles */
const customStyle = html`
<style>
- .container {
- --account-label-padding-horizontal: 6px;
- }
gr-button.remove::part(paper-button),
gr-button.remove:hover::part(paper-button),
gr-button.remove:focus::part(paper-button) {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_test.ts b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_test.ts
new file mode 100644
index 0000000..4e4811e
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_test.ts
@@ -0,0 +1,58 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import {fixture} from '@open-wc/testing-helpers';
+import {html} from 'lit';
+import './gr-account-chip';
+import {GrAccountChip} from './gr-account-chip';
+import {
+ createAccountWithIdNameAndEmail,
+ createChange,
+} from '../../../test/test-data-generators';
+
+suite('gr-account-chip tests', () => {
+ let element: GrAccountChip;
+ setup(async () => {
+ const reviewer = createAccountWithIdNameAndEmail();
+ const change = createChange();
+ element = await fixture<GrAccountChip>(html`<gr-account-chip
+ .account=${reviewer}
+ .change=${change}
+ ></gr-account-chip>`);
+ });
+
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(`<div class="container">
+ <gr-account-link></gr-account-link>
+ <slot name="vote-chip"></slot>
+ <gr-button
+ aria-disabled="false"
+ aria-label="Remove"
+ class="remove"
+ hidden=""
+ id="remove"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ <iron-icon icon="gr-icons:close"></iron-icon>
+ </gr-button>
+ </div>
+ `);
+ });
+});
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 dabf761..99e10ae 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
@@ -17,10 +17,11 @@
import '@polymer/iron-icon/iron-icon';
import '../gr-avatar/gr-avatar';
import '../gr-hovercard-account/gr-hovercard-account';
-import {appContext} from '../../../services/app-context';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param';
+import {getAppContext} from '../../../services/app-context';
import {getDisplayName} from '../../../utils/display-name-util';
import {isSelf, isServiceUser} from '../../../utils/account-util';
-import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
import {ChangeInfo, AccountInfo, ServerInfo} from '../../../types/common';
import {hasOwnProperty} from '../../../utils/common-util';
import {fireEvent} from '../../../utils/event-util';
@@ -102,9 +103,9 @@
@property({type: Boolean, reflect: true})
deselected = false;
- reporting: ReportingService;
+ readonly reporting = getAppContext().reportingService;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
static override get styles() {
return [
@@ -255,22 +256,22 @@
? html`<gr-avatar .account="${account}" imageSize="32"></gr-avatar>`
: ''}
<span class="text" part="gr-account-label-text">
- <span class="name"
- >${this._computeName(account, this.firstName, this._config)}</span
- >
+ <span class="name">
+ ${this._computeName(account, this.firstName, this._config)}
+ </span>
${!this.hideStatus && account.status
? html`<iron-icon
class="status"
- icon="gr-icons:calendar"
+ icon="gr-icons:unavailable"
></iron-icon>`
: ''}
+ ${this.renderAccountStatusPlugins()}
</span>
</span>`;
}
constructor() {
super();
- this.reporting = appContext.reportingService;
this.restApiService.getConfig().then(config => {
this._config = config;
});
@@ -283,6 +284,22 @@
});
}
+ // Note: account statuses from plugins are shown regardless of
+ // hideStatus setting
+ private renderAccountStatusPlugins() {
+ if (!this.account?._account_id) {
+ return;
+ }
+ return html`
+ <gr-endpoint-decorator name="account-status-icon">
+ <gr-endpoint-param
+ name="accountId"
+ .value="${this.account._account_id}"
+ ></gr-endpoint-param>
+ </gr-endpoint-decorator>
+ `;
+ }
+
handleKeyDown(e: KeyboardEvent) {
if (modifierPressed(e)) return;
// Only react to `return` and `space`.
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.ts b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.ts
index 574e450..edeb399 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.ts
@@ -52,6 +52,37 @@
};
});
+ test('renders', async () => {
+ element.account = kermit;
+ await element.updateComplete;
+ expect(element).shadowDom.to.equal(`<span>
+ <gr-hovercard-account for="hovercardTarget">
+ </gr-hovercard-account>
+ </span>
+ <span
+ id="hovercardTarget"
+ tabindex="0"
+ >
+ <gr-avatar
+ hidden=""
+ imagesize="32"
+ >
+ </gr-avatar>
+ <span
+ class="text"
+ part="gr-account-label-text"
+ >
+ <span class="name">
+ kermit
+ </span>
+ <gr-endpoint-decorator name="account-status-icon">
+ <gr-endpoint-param name="accountId"></gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </span>
+ </span>
+ `);
+ });
+
suite('_computeName', () => {
test('not showing anonymous', () => {
const account = {name: 'Wyatt'};
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.ts b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.ts
index c754e47..a78f32f 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.ts
@@ -16,19 +16,38 @@
*/
import '../../../test/common-test-setup-karma';
+import {fixture} from '@open-wc/testing-helpers';
+import {html} from 'lit';
import './gr-account-link';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {GrAccountLink} from './gr-account-link';
-import {createAccountWithId} from '../../../test/test-data-generators';
+import {
+ createAccountWithId,
+ createAccountWithIdNameAndEmail,
+} from '../../../test/test-data-generators';
import {AccountId, AccountInfo, EmailAddress} from '../../../types/common';
-const basicFixture = fixtureFromElement('gr-account-link');
-
suite('gr-account-link tests', () => {
let element: GrAccountLink;
- setup(() => {
- element = basicFixture.instantiate();
+ setup(async () => {
+ const account = createAccountWithIdNameAndEmail();
+ element = await fixture<GrAccountLink>(
+ html`<gr-account-link .account=${account}></gr-account-link>`
+ );
+ });
+
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(`<span>
+ <a href="">
+ <gr-account-label
+ deselected=""
+ exportparts="gr-account-label-text: gr-account-link-text"
+ >
+ </gr-account-label>
+ </a>
+ </span>
+ `);
});
test('computed fields', () => {
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 5449981..5f0cf7a 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
@@ -19,7 +19,7 @@
import '../../../styles/shared-styles';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-account-list_html';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {customElement, property} from '@polymer/decorators';
import {
ChangeInfo,
@@ -32,7 +32,6 @@
ReviewerSuggestionsProvider,
SuggestionItem,
} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
-import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
import {GrAccountEntry} from '../gr-account-entry/gr-account-entry';
import {GrAccountChip} from '../gr-account-chip/gr-account-chip';
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
@@ -178,13 +177,12 @@
@property({type: Object})
_querySuggestions: (input: string) => Promise<SuggestionItem[]>;
- reporting: ReportingService;
+ private readonly reporting = getAppContext().reportingService;
private pendingRemoval: Set<AccountInput> = new Set();
constructor() {
super();
- this.reporting = appContext.reportingService;
this._querySuggestions = input => this._getSuggestions(input);
this.addEventListener('remove', e =>
this._handleRemove(e as CustomEvent<{account: AccountInput}>)
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
index e7137e4..7216502 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.ts
@@ -103,19 +103,19 @@
override connectedCallback() {
super.connectedCallback();
this.cleanups.push(
- addShortcut(this, {key: Key.UP}, e => this._handleUp(e))
+ addShortcut(this, {key: Key.UP}, () => this._handleUp())
);
this.cleanups.push(
- addShortcut(this, {key: Key.DOWN}, e => this._handleDown(e))
+ addShortcut(this, {key: Key.DOWN}, () => this._handleDown())
);
this.cleanups.push(
- addShortcut(this, {key: Key.ENTER}, e => this._handleEnter(e))
+ addShortcut(this, {key: Key.ENTER}, () => this._handleEnter())
);
this.cleanups.push(
- addShortcut(this, {key: Key.ESC}, _ => this._handleEscape())
+ addShortcut(this, {key: Key.ESC}, () => this._handleEscape())
);
this.cleanups.push(
- addShortcut(this, {key: Key.TAB}, e => this._handleTab(e))
+ addShortcut(this, {key: Key.TAB}, () => this._handleTab())
);
}
@@ -141,37 +141,23 @@
return this.getCursorTarget()?.dataset['value'] || '';
}
- _handleUp(e: Event) {
- if (!this.isHidden) {
- e.preventDefault();
- e.stopPropagation();
- this.cursorUp();
- }
+ _handleUp() {
+ if (!this.isHidden) this.cursorUp();
}
- _handleDown(e: Event) {
- if (!this.isHidden) {
- e.preventDefault();
- e.stopPropagation();
- this.cursorDown();
- }
+ _handleDown() {
+ if (!this.isHidden) this.cursorDown();
}
cursorDown() {
- if (!this.isHidden) {
- this.cursor.next();
- }
+ if (!this.isHidden) this.cursor.next();
}
cursorUp() {
- if (!this.isHidden) {
- this.cursor.previous();
- }
+ if (!this.isHidden) this.cursor.previous();
}
- _handleTab(e: Event) {
- e.preventDefault();
- e.stopPropagation();
+ _handleTab() {
this.dispatchEvent(
new CustomEvent<ItemSelectedEvent>('item-selected', {
detail: {
@@ -184,9 +170,7 @@
);
}
- _handleEnter(e: Event) {
- e.preventDefault();
- e.stopPropagation();
+ _handleEnter() {
this.dispatchEvent(
new CustomEvent<ItemSelectedEvent>('item-selected', {
detail: {
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
index 8e84aa2..4be42d8 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
@@ -235,12 +235,25 @@
}
_handleItemSelect(e: CustomEvent) {
- // Let _handleKeydown deal with keyboard interaction.
- if (e.detail.trigger !== 'click') {
- return;
+ if (e.detail.trigger === 'click') {
+ this._selected = e.detail.selected;
+ this._commit();
+ e.stopPropagation();
+ e.preventDefault();
+ } else if (e.detail.trigger === 'enter') {
+ this._handleInputCommit();
+ e.stopPropagation();
+ e.preventDefault();
+ } else if (e.detail.trigger === 'tab') {
+ if (this.tabComplete) {
+ this._handleInputCommit(true);
+ e.stopPropagation();
+ e.preventDefault();
+ this.focus();
+ } else {
+ this._focused = false;
+ }
}
- this._selected = e.detail.selected;
- this._commit();
}
get _inputElement() {
@@ -351,8 +364,7 @@
}
/**
- * _handleKeydown used for key handling in the this.$.input AND all child
- * autocomplete options.
+ * _handleKeydown used for key handling in the this.$.input.
*/
_handleKeydown(e: KeyboardEvent) {
this._focused = true;
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.ts
index 62775aa..bdb65ea 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.ts
@@ -113,7 +113,6 @@
horizontal-align="left"
id="suggestions"
on-item-selected="_handleItemSelect"
- on-keydown="_handleKeydown"
suggestions="[[_suggestions]]"
role="listbox"
index="[[_index]]"
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
index 7da7ed5..3571bd2 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
@@ -515,7 +515,7 @@
assert.equal(element._suggestions.length, 1);
});
- test('tab on suggestion, tabComplete = false', () => {
+ test('tab on suggestion, tabComplete = false', async () => {
element._suggestions = [{name: 'sugar bombs'}];
element._focused = true;
// When tabComplete is false, do not focus.
@@ -528,14 +528,14 @@
queryAndAssert(suggestionsEl(), 'li:first-child'),
9,
null,
- 'tab'
+ 'Tab'
);
- flush();
+ await flush();
assert.isFalse(commitSpy.called);
assert.isFalse(element._focused);
});
- test('tab on suggestion, tabComplete = true', () => {
+ test('tab on suggestion, tabComplete = true', async () => {
element._suggestions = [{name: 'sugar bombs'}];
element._focused = true;
// When tabComplete is true, focus.
@@ -548,9 +548,9 @@
queryAndAssert(suggestionsEl(), 'li:first-child'),
9,
null,
- 'tab'
+ 'Tab'
);
- flush();
+ await flush();
assert.isTrue(commitSpy.called);
assert.isTrue(element._focused);
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 33bf6c6..34c553d 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.ts
@@ -17,7 +17,7 @@
import {getBaseUrl} from '../../../utils/url-util';
import {getPluginLoader} from '../gr-js-api-interface/gr-plugin-loader';
import {AccountInfo} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators';
@@ -32,7 +32,7 @@
@property({type: Boolean})
_hasAvatars = false;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
static override get styles() {
return [
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.ts b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.ts
index b3c485a..3aeef3e 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.ts
@@ -19,7 +19,7 @@
import './gr-avatar';
import {GrAvatar} from './gr-avatar';
import {getPluginLoader} from '../gr-js-api-interface/gr-plugin-loader';
-import {appContext} from '../../../services/app-context';
+import {getAppContext, AppContext} from '../../../services/app-context';
import {AvatarInfo} from '../../../types/common';
import {
createAccountWithEmail,
@@ -116,7 +116,9 @@
});
suite('config set', () => {
+ let appContext: AppContext;
setup(() => {
+ appContext = getAppContext();
const config = {
...createServerInfo(),
plugin: {has_avatars: true, js_resource_paths: []},
@@ -141,7 +143,7 @@
getPluginLoader().loadPlugins([]);
return Promise.all([
- appContext.restApiService.getConfig(),
+ appContext!.restApiService.getConfig(),
getPluginLoader().awaitPluginsLoaded(),
]).then(() => {
assert.isFalse(element.hasAttribute('hidden'));
@@ -154,9 +156,9 @@
});
suite('plugin has avatars', () => {
- let element: GrAvatar;
-
+ let appContext: AppContext;
setup(() => {
+ appContext = getAppContext();
const config = {
...createServerInfo(),
plugin: {has_avatars: true, js_resource_paths: []},
@@ -185,10 +187,11 @@
suite('config not set', () => {
let element: GrAvatar;
+ let appContext: AppContext;
setup(() => {
stub('gr-avatar', '_getConfig').returns(Promise.resolve(undefined));
-
+ appContext = getAppContext();
element = basicFixture.instantiate();
});
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 ea5b5bb..2da1b19 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
@@ -19,19 +19,23 @@
import {votingStyles} from '../../../styles/gr-voting-styles';
import {css, html, LitElement, PropertyValues} from 'lit';
import {customElement, property} from 'lit/decorators';
-import {getEventPath, modifierPressed} from '../../../utils/dom-util';
-import {appContext} from '../../../services/app-context';
-import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
+import {addShortcut, getEventPath, Key} from '../../../utils/dom-util';
+import {getAppContext} from '../../../services/app-context';
declare global {
interface HTMLElementTagNameMap {
'gr-button': GrButton;
}
}
-
+/**
+ * @attr {Boolean} no-uppercase - text in button is not uppercased
+ * @attr {Boolean} primary - set primary button color
+ * @attr {Boolean} secondary - set secondary button color
+ */
@customElement('gr-button')
export class GrButton extends LitElement {
- private readonly reporting: ReportingService = appContext.reportingService;
+ // Private but used in tests.
+ readonly reporting = getAppContext().reportingService;
/**
* Should this button be rendered as a vote chip? Then we are applying
@@ -203,7 +207,8 @@
super();
this.initialTabindex = this.getAttribute('tabindex') || '0';
this.addEventListener('click', e => this._handleAction(e));
- this.addEventListener('keydown', e => this._handleKeydown(e));
+ addShortcut(this, {key: Key.ENTER}, () => this.click());
+ addShortcut(this, {key: Key.SPACE}, () => this.click());
}
override updated(changedProperties: PropertyValues) {
@@ -241,14 +246,4 @@
this.reporting.reportInteraction('button-click', {path: getEventPath(e)});
}
-
- _handleKeydown(e: KeyboardEvent) {
- if (modifierPressed(e)) return;
- // Handle `enter`, `space`.
- if (e.keyCode === 13 || e.keyCode === 32) {
- e.preventDefault();
- e.stopPropagation();
- this.click();
- }
- }
}
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts
index 0149bd5..a9dcab1 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.ts
@@ -19,11 +19,11 @@
import '../../../test/common-test-setup-karma';
import './gr-button';
import {addListener} from '@polymer/polymer/lib/utils/gestures';
-import {appContext} from '../../../services/app-context';
import {html} from '@polymer/polymer/lib/utils/html-tag';
import {GrButton} from './gr-button';
-import {queryAndAssert} from '../../../test/test-utils';
+import {pressKey, queryAndAssert} from '../../../test/test-utils';
import {PaperButtonElement} from '@polymer/paper-button';
+import {Key, Modifier} from '../../../utils/dom-util';
const basicFixture = fixtureFromElement('gr-button');
@@ -55,6 +55,20 @@
await element.updateComplete;
});
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(`<paper-button
+ animated=""
+ aria-disabled="false"
+ elevation="1"
+ part="paper-button"
+ raised=""
+ role="button"
+ tabindex="-1"
+ ><slot></slot><i class="downArrow"></i>
+ </paper-button>
+ `);
+ });
+
test('disabled is set by disabled', async () => {
const paperBtn = queryAndAssert<PaperButtonElement>(
element,
@@ -143,23 +157,22 @@
assert.isTrue(spy.calledOnce);
});
- // Keycodes: 32 for Space, 13 for Enter.
- for (const key of [32, 13]) {
- test(`dispatches click event on keycode ${key}`, () => {
+ for (const key of [Key.ENTER, Key.SPACE]) {
+ test(`dispatches click event on key '${key}'`, () => {
const tapSpy = sinon.spy();
element.addEventListener('click', tapSpy);
- MockInteractions.pressAndReleaseKeyOn(element, key);
+ pressKey(element, key);
assert.isTrue(tapSpy.calledOnce);
});
- test(`dispatches no click event with modifier on keycode ${key}`, () => {
+ test(`dispatches no click event with modifier on key '${key}'`, () => {
const tapSpy = sinon.spy();
element.addEventListener('click', tapSpy);
- MockInteractions.pressAndReleaseKeyOn(element, key, 'shift');
- MockInteractions.pressAndReleaseKeyOn(element, key, 'ctrl');
- MockInteractions.pressAndReleaseKeyOn(element, key, 'meta');
- MockInteractions.pressAndReleaseKeyOn(element, key, 'alt');
- assert.isFalse(tapSpy.calledOnce);
+ pressKey(element, key, Modifier.ALT_KEY);
+ pressKey(element, key, Modifier.CTRL_KEY);
+ pressKey(element, key, Modifier.META_KEY);
+ pressKey(element, key, Modifier.SHIFT_KEY);
+ assert.isFalse(tapSpy.called);
});
}
@@ -177,12 +190,11 @@
});
}
- // Keycodes: 32 for Space, 13 for Enter.
- for (const key of [32, 13]) {
+ for (const key of [Key.ENTER, Key.SPACE]) {
test(`stops click event on keycode ${key}`, () => {
const tapSpy = sinon.spy();
element.addEventListener('click', tapSpy);
- MockInteractions.pressAndReleaseKeyOn(element, key);
+ pressKey(element, key);
assert.isFalse(tapSpy.called);
});
}
@@ -191,7 +203,7 @@
suite('reporting', () => {
let reportStub: sinon.SinonStub;
setup(() => {
- reportStub = sinon.stub(appContext.reportingService, 'reportInteraction');
+ reportStub = sinon.stub(element.reporting, 'reportInteraction');
reportStub.reset();
});
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
index a23621e..560c82c 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
@@ -15,17 +15,16 @@
* limitations under the License.
*/
import '../gr-icons/gr-icons';
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-change-star_html';
-import {customElement, property} from '@polymer/decorators';
import {ChangeInfo} from '../../../types/common';
import {fireAlert} from '../../../utils/event-util';
import {
Shortcut,
ShortcutSection,
} from '../../../services/shortcuts/shortcuts-config';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
declare global {
interface HTMLElementTagNameMap {
@@ -39,44 +38,78 @@
}
@customElement('gr-change-star')
-export class GrChangeStar extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrChangeStar extends LitElement {
/**
* Fired when star state is toggled.
*
* @event toggle-star
*/
- @property({type: Object, notify: true})
+ @property({type: Object})
change?: ChangeInfo;
- private readonly shortcuts = appContext.shortcutsService;
+ private readonly shortcuts = getAppContext().shortcutsService;
- _computeStarClass(starred?: boolean) {
- return starred ? 'active' : '';
+ static override get styles() {
+ return [
+ sharedStyles,
+ css`
+ button {
+ background-color: transparent;
+ cursor: pointer;
+ }
+ iron-icon.active {
+ fill: var(--link-color);
+ }
+ iron-icon {
+ vertical-align: top;
+ --iron-icon-height: var(
+ --gr-change-star-size,
+ var(--line-height-normal, 20px)
+ );
+ --iron-icon-width: var(
+ --gr-change-star-size,
+ var(--line-height-normal, 20px)
+ );
+ }
+ :host([hidden]) {
+ visibility: hidden;
+ display: block !important;
+ }
+ `,
+ ];
}
- _computeStarIcon(starred?: boolean) {
- // Hollow star is used to indicate inactive state.
- return `gr-icons:star${starred ? '' : '-border'}`;
- }
-
- _computeAriaLabel(starred?: boolean) {
- return starred ? 'Unstar this change' : 'Star this change';
+ override render() {
+ return html`
+ <button
+ role="checkbox"
+ title=${this.shortcuts.createTitle(
+ Shortcut.TOGGLE_CHANGE_STAR,
+ ShortcutSection.ACTIONS
+ )}
+ aria-label=${this.change?.starred
+ ? 'Unstar this change'
+ : 'Star this change'}
+ @click=${this.toggleStar}
+ >
+ <iron-icon
+ class=${this.change?.starred ? 'active' : ''}
+ .icon=${`gr-icons:star${this.change?.starred ? '' : '-border'}`}
+ ></iron-icon>
+ </button>
+ `;
}
toggleStar() {
// Note: change should always be defined when use gr-change-star
// but since we don't have a good way to enforce usage to always
// set the change, we still check it here.
- if (!this.change) {
- return;
- }
+ if (!this.change) return;
+
const newVal = !this.change.starred;
- this.set('change.starred', newVal);
+ this.change.starred = newVal;
+ this.requestUpdate('change');
const detail: ChangeStarToggleStarDetail = {
change: this.change,
starred: newVal,
@@ -90,8 +123,4 @@
})
);
}
-
- createTitle(shortcutName: Shortcut, section: ShortcutSection) {
- return this.shortcuts.createTitle(shortcutName, section);
- }
}
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.ts
deleted file mode 100644
index d404795..0000000
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- button {
- background-color: transparent;
- cursor: pointer;
- }
- iron-icon.active {
- fill: var(--link-color);
- }
- iron-icon {
- vertical-align: top;
- --iron-icon-height: var(
- --gr-change-star-size,
- var(--line-height-normal, 20px)
- );
- --iron-icon-width: var(
- --gr-change-star-size,
- var(--line-height-normal, 20px)
- );
- }
- :host([hidden]) {
- visibility: hidden;
- display: block !important;
- }
- </style>
- <button
- role="checkbox"
- title="[[createTitle(Shortcut.TOGGLE_CHANGE_STAR,
- ShortcutSection.ACTIONS)]]"
- aria-label="[[_computeAriaLabel(change.starred)]]"
- on-click="toggleStar"
- >
- <iron-icon
- class$="[[_computeStarClass(change.starred)]]"
- icon$="[[_computeStarIcon(change.starred)]]"
- ></iron-icon>
- </button>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts
index 8f411ae..2c5d7a2 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts
@@ -27,45 +27,47 @@
suite('gr-change-star tests', () => {
let element: GrChangeStar;
- setup(() => {
+ setup(async () => {
element = basicFixture.instantiate();
element.change = {
...createChange(),
starred: true,
};
+ await element.updateComplete;
});
test('star visibility states', async () => {
- element.set('change.starred', true);
- await flush();
+ element.change!.starred = true;
+ await element.updateComplete;
let icon = queryAndAssert<IronIconElement>(element, 'iron-icon');
assert.isTrue(icon.classList.contains('active'));
assert.equal(icon.icon, 'gr-icons:star');
- element.set('change.starred', false);
- await flush();
+ element.change!.starred = false;
+ element.requestUpdate('change');
+ await element.updateComplete;
icon = queryAndAssert<IronIconElement>(element, 'iron-icon');
assert.isFalse(icon.classList.contains('active'));
assert.equal(icon.icon, 'gr-icons:star-border');
});
test('starring', async () => {
- element.set('change.starred', false);
- await flush();
+ element.change!.starred = false;
+ await element.updateComplete;
assert.equal(element.change!.starred, false);
- MockInteractions.tap(queryAndAssert(element, 'button'));
- await flush();
+ MockInteractions.tap(queryAndAssert<HTMLButtonElement>(element, 'button'));
+ await element.updateComplete;
assert.equal(element.change!.starred, true);
});
test('unstarring', async () => {
- element.set('change.starred', true);
- await flush();
+ element.change!.starred = true;
+ await element.updateComplete;
assert.equal(element.change!.starred, true);
- MockInteractions.tap(queryAndAssert(element, 'button'));
- await flush();
+ MockInteractions.tap(queryAndAssert<HTMLButtonElement>(element, 'button'));
+ await element.updateComplete;
assert.equal(element.change!.starred, false);
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
index 0bd02d5..172518b 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
@@ -39,9 +39,9 @@
WIP = 'WIP',
}
-const WIP_TOOLTIP =
+export const WIP_TOOLTIP =
"This change isn't ready to be reviewed or submitted. " +
- "It will not appear on dashboards unless you are CC'ed or assigned, " +
+ "It will not appear on dashboards unless you are CC'ed, " +
'and email notifications will be silenced until the review is started.';
export const MERGE_CONFLICT_TOOLTIP =
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.ts
index 39fc7c6..f6e19aa 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.ts
@@ -18,17 +18,12 @@
import '../../../test/common-test-setup-karma';
import {createChange} from '../../../test/test-data-generators';
import './gr-change-status';
-import {ChangeStates, GrChangeStatus} from './gr-change-status';
+import {ChangeStates, GrChangeStatus, WIP_TOOLTIP} from './gr-change-status';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {MERGE_CONFLICT_TOOLTIP} from './gr-change-status';
const basicFixture = fixtureFromElement('gr-change-status');
-const WIP_TOOLTIP =
- "This change isn't ready to be reviewed or submitted. " +
- "It will not appear on dashboards unless you are CC'ed or assigned, " +
- 'and email notifications will be silenced until the review is started.';
-
const PRIVATE_TOOLTIP =
'This change is only visible to its owner and ' +
'current reviewers (or anyone with "View Private Changes" permission).';
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 3f8264b..ee40ad5 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
@@ -19,254 +19,653 @@
import '../gr-comment/gr-comment';
import '../../diff/gr-diff/gr-diff';
import '../gr-copy-clipboard/gr-copy-clipboard';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-comment-thread_html';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property, query, queryAll, state} from 'lit/decorators';
import {
computeDiffFromContext,
- computeId,
- DraftInfo,
isDraft,
isRobot,
- sortComments,
- UIComment,
- UIDraft,
- UIRobot,
+ Comment,
+ CommentThread,
+ getLastComment,
+ UnsavedInfo,
+ isDraftOrUnsaved,
+ createUnsavedComment,
+ getFirstComment,
+ createUnsavedReply,
+ isUnsaved,
} from '../../../utils/comment-util';
+import {ChangeMessageId} from '../../../api/rest-api';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {
- CommentSide,
createDefaultDiffPrefs,
- Side,
SpecialFilePath,
} from '../../../constants/constants';
import {computeDisplayPath} from '../../../utils/path-list-util';
-import {computed, customElement, observe, property} from '@polymer/decorators';
import {
AccountDetailInfo,
CommentRange,
- ConfigInfo,
NumericChangeId,
- PatchSetNum,
RepoName,
UrlEncodedCommentId,
} from '../../../types/common';
import {GrComment} from '../gr-comment/gr-comment';
-import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
-import {FILE, LineNumber} from '../../diff/gr-diff/gr-diff-line';
+import {FILE} from '../../diff/gr-diff/gr-diff-line';
import {GrButton} from '../gr-button/gr-button';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
import {DiffLayer, RenderPreferences} from '../../../api/diff';
-import {
- assertIsDefined,
- check,
- queryAndAssert,
-} from '../../../utils/common-util';
-import {fireAlert, waitForEventOnce} from '../../../utils/event-util';
+import {assertIsDefined} from '../../../utils/common-util';
+import {fire, fireAlert, waitForEventOnce} from '../../../utils/event-util';
import {GrSyntaxLayer} from '../../diff/gr-syntax-layer/gr-syntax-layer';
-import {StorageLocation} from '../../../services/storage/gr-storage';
import {TokenHighlightLayer} from '../../diff/gr-diff-builder/token-highlight-layer';
import {anyLineTooLong} from '../../diff/gr-diff/gr-diff-utils';
import {getUserName} from '../../../utils/display-name-util';
import {generateAbsoluteUrl} from '../../../utils/url-util';
-import {addGlobalShortcut} from '../../../utils/dom-util';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {a11yStyles} from '../../../styles/gr-a11y-styles';
+import {subscribe} from '../../lit/subscription-controller';
+import {repeat} from 'lit/directives/repeat';
+import {classMap} from 'lit/directives/class-map';
+import {ShortcutController} from '../../lit/shortcut-controller';
+import {ValueChangedEvent} from '../../../types/events';
+import {notDeepEqual} from '../../../utils/deep-util';
+import {resolve} from '../../../models/dependency';
+import {commentsModelToken} from '../../../models/comments/comments-model';
-const UNRESOLVED_EXPAND_COUNT = 5;
const NEWLINE_PATTERN = /\n/g;
-export interface GrCommentThread {
- $: {
- replyBtn: GrButton;
- quoteBtn: GrButton;
- };
+declare global {
+ interface HTMLElementEventMap {
+ 'comment-thread-editing-changed': ValueChangedEvent<boolean>;
+ }
}
+/**
+ * gr-comment-thread exposes the following attributes that allow a
+ * diff widget like gr-diff to show the thread in the right location:
+ *
+ * line-num:
+ * 1-based line number or 'FILE' if it refers to the entire file.
+ *
+ * diff-side:
+ * "left" or "right". These indicate which of the two diffed versions
+ * the comment relates to. In the case of unified diff, the left
+ * version is the one whose line number column is further to the left.
+ *
+ * range:
+ * The range of text that the comment refers to (start_line,
+ * start_character, end_line, end_character), serialized as JSON. If
+ * set, range's end_line will have the same value as line-num. Line
+ * numbers are 1-based, char numbers are 0-based. The start position
+ * (start_line, start_character) is inclusive, and the end position
+ * (end_line, end_character) is exclusive.
+ */
@customElement('gr-comment-thread')
-export class GrCommentThread extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
+export class GrCommentThread extends LitElement {
+ @query('#replyBtn')
+ replyBtn?: GrButton;
+
+ @query('#quoteBtn')
+ quoteBtn?: GrButton;
+
+ @query('.comment-box')
+ commentBox?: HTMLElement;
+
+ @queryAll('gr-comment')
+ commentElements?: NodeList;
/**
- * gr-comment-thread exposes the following attributes that allow a
- * diff widget like gr-diff to show the thread in the right location:
+ * Required to be set by parent.
*
- * line-num:
- * 1-based line number or 'FILE' if it refers to the entire file.
- *
- * diff-side:
- * "left" or "right". These indicate which of the two diffed versions
- * the comment relates to. In the case of unified diff, the left
- * version is the one whose line number column is further to the left.
- *
- * range:
- * The range of text that the comment refers to (start_line,
- * start_character, end_line, end_character), serialized as JSON. If
- * set, range's end_line will have the same value as line-num. Line
- * numbers are 1-based, char numbers are 0-based. The start position
- * (start_line, start_character) is inclusive, and the end position
- * (end_line, end_character) is exclusive.
+ * Lit's `hasChanged` change detection defaults to just checking strict
+ * equality (===). Here it makes sense to install a proper `deepEqual`
+ * check, because of how the comments-model and ChangeComments are setup:
+ * Each thread object is recreated on the slightest model change. So when you
+ * have 100 comment threads and there is an update to one thread, then you
+ * want to avoid re-rendering the other 99 threads.
*/
- @property({type: Number})
- changeNum?: NumericChangeId;
+ @property({hasChanged: notDeepEqual})
+ thread?: CommentThread;
- @property({type: Array})
- comments: UIComment[] = [];
-
- @property({type: Object, reflectToAttribute: true})
- range?: CommentRange;
-
- @property({type: String, reflectToAttribute: true})
- diffSide?: Side;
-
+ /**
+ * Id of the first comment and thus must not change. Will be derived from
+ * the `thread` property in the first willUpdate() cycle.
+ *
+ * The `rootId` property is also used in gr-diff for maintaining lists and
+ * maps of threads and their associated elements.
+ *
+ * Only stays `undefined` for new threads that only have an unsaved comment.
+ */
@property({type: String})
- patchNum?: PatchSetNum;
-
- @property({type: String})
- path: string | undefined;
-
- @property({type: String, observer: '_projectNameChanged'})
- projectName?: RepoName;
-
- @property({type: Boolean, notify: true, reflectToAttribute: true})
- hasDraft?: boolean;
-
- @property({type: Boolean})
- isOnParent = false;
-
- @property({type: Number})
- parentIndex: number | null = null;
-
- @property({
- type: String,
- notify: true,
- computed: '_computeRootId(comments.*)',
- })
rootId?: UrlEncodedCommentId;
- @property({type: Boolean, observer: 'handleShouldScrollIntoViewChanged'})
+ // TODO: Is this attribute needed for querySelector() or css rules?
+ // We don't need this internally for the component.
+ @property({type: Boolean, reflect: true, attribute: 'has-draft'})
+ hasDraft?: boolean;
+
+ /** Will be inspected on firstUpdated() only. */
+ @property({type: Boolean, attribute: 'should-scroll-into-view'})
shouldScrollIntoView = false;
- @property({type: Boolean})
+ /**
+ * Should the file path and line number be rendered above the comment thread
+ * widget? Typically true in <gr-thread-list> and false in <gr-diff>.
+ */
+ @property({type: Boolean, attribute: 'show-file-path'})
showFilePath = false;
- @property({type: Object, reflectToAttribute: true})
- lineNum?: LineNumber;
+ /**
+ * Only relevant when `showFilePath` is set.
+ * If false, then only the line number is rendered.
+ */
+ @property({type: Boolean, attribute: 'show-file-name'})
+ showFileName = false;
- @property({type: Boolean, notify: true, reflectToAttribute: true})
- unresolved?: boolean;
+ @property({type: Boolean, attribute: 'show-ported-comment'})
+ showPortedComment = false;
- @property({type: Boolean})
- _showActions?: boolean;
+ /** This is set to false by <gr-diff>. */
+ @property({type: Boolean, attribute: false})
+ showPatchset = true;
- @property({type: Object})
- _lastComment?: UIComment;
+ @property({type: Boolean, attribute: 'show-comment-context'})
+ showCommentContext = false;
- @property({type: Array})
- _orderedComments: UIComment[] = [];
+ /**
+ * Optional context information when a thread is being displayed for a
+ * specific change message. That influences which comments are expanded or
+ * collapsed by default.
+ */
+ @property({type: String, attribute: 'message-id'})
+ messageId?: ChangeMessageId;
- @property({type: Object})
- _projectConfig?: ConfigInfo;
+ /**
+ * We are reflecting the editing state of the draft comment here. This is not
+ * an input property, but can be inspected from the parent component.
+ *
+ * Changes to this property are fired as 'comment-thread-editing-changed'
+ * events.
+ */
+ @property({type: Boolean, attribute: 'false'})
+ editing = false;
- @property({type: Object})
- _prefs: DiffPreferencesInfo = createDefaultDiffPrefs();
+ /**
+ * This can either be an unsaved reply to the last comment or the unsaved
+ * content of a brand new comment thread (then `comments` is empty).
+ * If set, then `thread.comments` must not contain a draft. A thread can only
+ * contain *either* an unsaved comment *or* a draft, not both.
+ */
+ @state()
+ unsavedComment?: UnsavedInfo;
- @property({type: Object})
- _renderPrefs: RenderPreferences = {
+ @state()
+ changeNum?: NumericChangeId;
+
+ @state()
+ prefs: DiffPreferencesInfo = createDefaultDiffPrefs();
+
+ @state()
+ renderPrefs: RenderPreferences = {
hide_left_side: true,
disable_context_control_buttons: true,
show_file_comment_button: false,
hide_line_length_indicator: true,
};
- @property({type: Boolean, reflectToAttribute: true})
- isRobotComment = false;
+ @state()
+ repoName?: RepoName;
- @property({type: Boolean})
- showFileName = true;
+ @state()
+ account?: AccountDetailInfo;
- @property({type: Boolean})
- showPortedComment = false;
-
- @property({type: Boolean})
- showPatchset = true;
-
- @property({type: Boolean})
- showCommentContext = false;
-
- @property({type: Object})
- _selfAccount?: AccountDetailInfo;
-
- @property({type: Array})
+ @state()
layers: DiffLayer[] = [];
- /** Called in disconnectedCallback. */
- private cleanups: (() => void)[] = [];
+ /** Computed during willUpdate(). */
+ @state()
+ diff?: DiffInfo;
- private readonly reporting = appContext.reportingService;
+ /** Computed during willUpdate(). */
+ @state()
+ highlightRange?: CommentRange;
- private readonly commentsService = appContext.commentsService;
+ /**
+ * Reflects the *dirty* state of whether the thread is currently unresolved.
+ * We are listening on the <gr-comment> of the draft, so we even know when the
+ * checkbox is checked, even if not yet saved.
+ */
+ @state()
+ unresolved = true;
- readonly storage = appContext.storageService;
+ /**
+ * Normally drafts are saved within the <gr-comment> child component and we
+ * don't care about that. But when creating 'Done.' replies we are actually
+ * saving from this component. True while the REST API call is inflight.
+ */
+ @state()
+ saving = false;
+
+ // Private but used in tests.
+ readonly getCommentsModel = resolve(this, commentsModelToken);
+
+ private readonly changeModel = getAppContext().changeModel;
+
+ private readonly userModel = getAppContext().userModel;
+
+ private readonly shortcuts = new ShortcutController(this);
private readonly syntaxLayer = new GrSyntaxLayer();
- readonly restApiService = appContext.restApiService;
-
- private readonly shortcuts = appContext.shortcutsService;
-
constructor() {
super();
- this.addEventListener('comment-update', e =>
- this._handleCommentUpdate(e as CustomEvent)
+ subscribe(this, this.changeModel.changeNum$, x => (this.changeNum = x));
+ subscribe(this, this.userModel.account$, x => (this.account = x));
+ subscribe(this, this.changeModel.repo$, x => (this.repoName = x));
+ subscribe(this, this.userModel.diffPreferences$, x =>
+ this.syntaxLayer.setEnabled(!!x.syntax_highlighting)
);
- appContext.restApiService.getPreferences().then(prefs => {
- this._initLayers(!!prefs?.disable_token_highlighting);
+ subscribe(this, this.userModel.preferences$, prefs => {
+ const layers: DiffLayer[] = [this.syntaxLayer];
+ if (!prefs.disable_token_highlighting) {
+ layers.push(new TokenHighlightLayer(this));
+ }
+ this.layers = layers;
});
- }
-
- override disconnectedCallback() {
- super.disconnectedCallback();
- for (const cleanup of this.cleanups) cleanup();
- this.cleanups = [];
- }
-
- override connectedCallback() {
- super.connectedCallback();
- this.cleanups.push(
- addGlobalShortcut({key: 'e'}, e => this.handleExpandShortcut(e))
- );
- this.cleanups.push(
- addGlobalShortcut({key: 'E'}, e => this.handleCollapseShortcut(e))
- );
- this._getLoggedIn().then(loggedIn => {
- this._showActions = loggedIn;
- });
- this.restApiService.getDiffPreferences().then(prefs => {
- if (!prefs) return;
- this._prefs = {
+ subscribe(this, this.userModel.diffPreferences$, prefs => {
+ this.prefs = {
...prefs,
// set line_wrapping to true so that the context can take all the
// remaining space after comment card has rendered
line_wrapping: true,
};
- this.syntaxLayer.setEnabled(!!prefs.syntax_highlighting);
});
- this.restApiService.getAccount().then(account => {
- this._selfAccount = account;
- });
- this._setInitialExpandedState();
+ this.shortcuts.addGlobal({key: 'e'}, () => this.handleExpandShortcut());
+ this.shortcuts.addGlobal({key: 'E'}, () => this.handleCollapseShortcut());
}
- @computed('comments', 'path')
- get _diff() {
- if (this.comments === undefined || this.path === undefined) return;
- if (!this.comments[0]?.context_lines?.length) return;
- const diff = computeDiffFromContext(
- this.comments[0].context_lines,
- this.path,
- this.comments[0].source_content_type
+ static override get styles() {
+ return [
+ a11yStyles,
+ sharedStyles,
+ css`
+ :host {
+ font-family: var(--font-family);
+ font-size: var(--font-size-normal);
+ font-weight: var(--font-weight-normal);
+ line-height: var(--line-height-normal);
+ /* Explicitly set the background color of the diff. We
+ * cannot use the diff content type ab because of the skip chunk preceding
+ * it, diff processor assumes the chunk of type skip/ab can be collapsed
+ * and hides our diff behind context control buttons.
+ * */
+ --dark-add-highlight-color: var(--background-color-primary);
+ }
+ gr-button {
+ margin-left: var(--spacing-m);
+ }
+ gr-comment {
+ border-bottom: 1px solid var(--comment-separator-color);
+ }
+ #actions {
+ margin-left: auto;
+ padding: var(--spacing-s) var(--spacing-m);
+ }
+ .comment-box {
+ width: 80ch;
+ max-width: 100%;
+ background-color: var(--comment-background-color);
+ color: var(--comment-text-color);
+ box-shadow: var(--elevation-level-2);
+ border-radius: var(--border-radius);
+ flex-shrink: 0;
+ }
+ #container {
+ display: var(--gr-comment-thread-display, flex);
+ align-items: flex-start;
+ margin: 0 var(--spacing-s) var(--spacing-s);
+ white-space: normal;
+ /** This is required for firefox to continue the inheritance */
+ -webkit-user-select: inherit;
+ -moz-user-select: inherit;
+ -ms-user-select: inherit;
+ user-select: inherit;
+ }
+ .comment-box.unresolved {
+ background-color: var(--unresolved-comment-background-color);
+ }
+ .comment-box.robotComment {
+ background-color: var(--robot-comment-background-color);
+ }
+ #actionsContainer {
+ display: flex;
+ }
+ .comment-box.saving #actionsContainer {
+ opacity: 0.5;
+ }
+ #unresolvedLabel {
+ font-family: var(--font-family);
+ margin: auto 0;
+ padding: var(--spacing-m);
+ }
+ .pathInfo {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ padding: 0 var(--spacing-s) var(--spacing-s);
+ }
+ .fileName {
+ padding: var(--spacing-m) var(--spacing-s) var(--spacing-m);
+ }
+ @media only screen and (max-width: 1200px) {
+ .diff-container {
+ display: none;
+ }
+ }
+ .diff-container {
+ margin-left: var(--spacing-l);
+ border: 1px solid var(--border-color);
+ flex-grow: 1;
+ flex-shrink: 1;
+ max-width: 1200px;
+ }
+ .view-diff-button {
+ margin: var(--spacing-s) var(--spacing-m);
+ }
+ .view-diff-container {
+ border-top: 1px solid var(--border-color);
+ background-color: var(--background-color-primary);
+ }
+
+ /* In saved state the "reply" and "quote" buttons are 28px height.
+ * top:4px positions the 20px icon vertically centered.
+ * Currently in draft state the "save" and "cancel" buttons are 20px
+ * height, so the link icon does not need a top:4px in gr-comment_html.
+ */
+ .link-icon {
+ position: relative;
+ top: 4px;
+ cursor: pointer;
+ }
+ .fileName gr-copy-clipboard {
+ display: inline-block;
+ visibility: hidden;
+ vertical-align: top;
+ --gr-button-padding: 0px;
+ }
+ .fileName:focus-within gr-copy-clipboard,
+ .fileName:hover gr-copy-clipboard {
+ visibility: visible;
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ if (!this.thread) return;
+ const dynamicBoxClasses = {
+ robotComment: this.isRobotComment(),
+ unresolved: this.unresolved,
+ saving: this.saving,
+ };
+ return html`
+ ${this.renderFilePath()}
+ <div id="container">
+ <h3 class="assistive-tech-only">${this.computeAriaHeading()}</h3>
+ <div class="comment-box ${classMap(dynamicBoxClasses)}" tabindex="0">
+ ${this.renderComments()} ${this.renderActions()}
+ </div>
+ ${this.renderContextualDiff()}
+ </div>
+ `;
+ }
+
+ renderFilePath() {
+ if (!this.showFilePath) return;
+ const href = this.getUrlForFileComment();
+ const line = this.computeDisplayLine();
+ return html`
+ ${this.renderFileName()}
+ <div class="pathInfo">
+ ${href
+ ? html`<a href="${href}">${line}</a>`
+ : html`<span>${line}</span>`}
+ </div>
+ `;
+ }
+
+ renderFileName() {
+ if (!this.showFileName) return;
+ if (this.isPatchsetLevel()) {
+ return html`<div class="fileName"><span>Patchset</span></div>`;
+ }
+ const href = this.getDiffUrlForPath();
+ const displayPath = this.getDisplayPath();
+ return html`
+ <div class="fileName">
+ ${href
+ ? html`<a href="${href}">${displayPath}</a>`
+ : html`<span>${displayPath}</span>`}
+ <gr-copy-clipboard hideInput .text="${displayPath}"></gr-copy-clipboard>
+ </div>
+ `;
+ }
+
+ renderComments() {
+ assertIsDefined(this.thread, 'thread');
+ const robotButtonDisabled = !this.account || this.isDraftOrUnsaved();
+ const comments: Comment[] = [...this.thread.comments];
+ if (this.unsavedComment && !this.isDraft()) {
+ comments.push(this.unsavedComment);
+ }
+ return repeat(
+ comments,
+ // We want to reuse <gr-comment> when unsaved changes to draft.
+ comment => (isDraftOrUnsaved(comment) ? 'unsaved' : comment.id),
+ comment => {
+ const initiallyCollapsed =
+ !isDraftOrUnsaved(comment) &&
+ (this.messageId
+ ? comment.change_message_id !== this.messageId
+ : !this.unresolved);
+ return html`
+ <gr-comment
+ .comment="${comment}"
+ .comments="${this.thread!.comments}"
+ ?initially-collapsed="${initiallyCollapsed}"
+ ?robot-button-disabled="${robotButtonDisabled}"
+ ?show-patchset="${this.showPatchset}"
+ ?show-ported-comment="${this.showPortedComment &&
+ comment.id === this.rootId}"
+ @create-fix-comment="${this.handleCommentFix}"
+ @copy-comment-link="${this.handleCopyLink}"
+ @comment-editing-changed="${(e: CustomEvent) => {
+ if (isDraftOrUnsaved(comment)) this.editing = e.detail;
+ }}"
+ @comment-unresolved-changed="${(e: CustomEvent) => {
+ if (isDraftOrUnsaved(comment)) this.unresolved = e.detail;
+ }}"
+ ></gr-comment>
+ `;
+ }
);
+ }
+
+ renderActions() {
+ if (!this.account || this.isDraftOrUnsaved() || this.isRobotComment())
+ return;
+ return html`
+ <div id="actionsContainer">
+ <span id="unresolvedLabel">${
+ this.unresolved ? 'Unresolved' : 'Resolved'
+ }</span>
+ <div id="actions">
+ <iron-icon
+ class="link-icon copy"
+ @click="${this.handleCopyLink}"
+ title="Copy link to this comment"
+ icon="gr-icons:link"
+ role="button"
+ tabindex="0"
+ >
+ </iron-icon>
+ <gr-button
+ id="replyBtn"
+ link
+ class="action reply"
+ ?disabled="${this.saving}"
+ @click="${() => this.handleCommentReply(false)}"
+ >Reply</gr-button
+ >
+ <gr-button
+ id="quoteBtn"
+ link
+ class="action quote"
+ ?disabled="${this.saving}"
+ @click="${() => this.handleCommentReply(true)}"
+ >Quote</gr-button
+ >
+ ${
+ this.unresolved
+ ? html`
+ <gr-button
+ id="ackBtn"
+ link
+ class="action ack"
+ ?disabled="${this.saving}"
+ @click="${this.handleCommentAck}"
+ >Ack</gr-button
+ >
+ <gr-button
+ id="doneBtn"
+ link
+ class="action done"
+ ?disabled="${this.saving}"
+ @click="${this.handleCommentDone}"
+ >Done</gr-button
+ >
+ `
+ : ''
+ }
+ </div>
+ </div>
+ </div>
+ `;
+ }
+
+ renderContextualDiff() {
+ if (!this.changeNum || !this.showCommentContext || !this.diff) return;
+ if (!this.thread?.path) return;
+ const href = this.getUrlForFileComment();
+ return html`
+ <div class="diff-container">
+ <gr-diff
+ id="diff"
+ .changeNum="${this.changeNum}"
+ .diff="${this.diff}"
+ .layers="${this.layers}"
+ .path="${this.thread.path}"
+ .prefs="${this.prefs}"
+ .renderPrefs="${this.renderPrefs}"
+ .highlightRange="${this.highlightRange}"
+ >
+ </gr-diff>
+ <div class="view-diff-container">
+ <a href="${href}">
+ <gr-button link class="view-diff-button">View Diff</gr-button>
+ </a>
+ </div>
+ </div>
+ `;
+ }
+
+ private firstWillUpdateDone = false;
+
+ firstWillUpdate() {
+ if (!this.thread) return;
+ if (this.firstWillUpdateDone) return;
+ this.firstWillUpdateDone = true;
+
+ if (this.getFirstComment() === undefined) {
+ this.unsavedComment = createUnsavedComment(this.thread);
+ }
+ this.unresolved = this.getLastComment()?.unresolved ?? true;
+ this.diff = this.computeDiff();
+ this.highlightRange = this.computeHighlightRange();
+ }
+
+ override willUpdate(changed: PropertyValues) {
+ this.firstWillUpdate();
+ if (changed.has('thread')) {
+ if (!this.isDraftOrUnsaved()) {
+ // We can only do this for threads without draft, because otherwise we
+ // are relying on the <gr-comment> component for the draft to fire
+ // events about the *dirty* `unresolved` state.
+ this.unresolved = this.getLastComment()?.unresolved ?? true;
+ }
+ this.hasDraft = this.isDraftOrUnsaved();
+ this.rootId = this.getFirstComment()?.id;
+ if (this.isDraft()) {
+ this.unsavedComment = undefined;
+ }
+ }
+ if (changed.has('editing')) {
+ // changed.get('editing') contains the old value. We only want to trigger
+ // when changing from editing to non-editing (user has cancelled/saved).
+ // We do *not* want to trigger on first render (old value is `null`)
+ if (!this.editing && changed.get('editing') === true) {
+ this.unsavedComment = undefined;
+ if (this.thread?.comments.length === 0) {
+ this.remove();
+ }
+ }
+ fire(this, 'comment-thread-editing-changed', {value: this.editing});
+ }
+ }
+
+ override firstUpdated() {
+ if (this.shouldScrollIntoView) {
+ this.commentBox?.focus();
+ this.scrollIntoView();
+ }
+ }
+
+ private isDraft() {
+ return isDraft(this.getLastComment());
+ }
+
+ private isDraftOrUnsaved(): boolean {
+ return this.isDraft() || this.isUnsaved();
+ }
+
+ private isNewThread(): boolean {
+ return this.thread?.comments.length === 0;
+ }
+
+ private isUnsaved(): boolean {
+ return !!this.unsavedComment || this.thread?.comments.length === 0;
+ }
+
+ private isPatchsetLevel() {
+ return this.thread?.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
+ }
+
+ private computeDiff() {
+ if (!this.showCommentContext) return;
+ if (!this.thread?.path) return;
+ const firstComment = this.getFirstComment();
+ if (!firstComment?.context_lines?.length) return;
+ const diff = computeDiffFromContext(
+ firstComment.context_lines,
+ this.thread?.path,
+ firstComment.source_content_type
+ );
+ // Do we really have to re-compute (and re-render) the diff?
+ if (this.diff && JSON.stringify(this.diff) === JSON.stringify(diff)) {
+ return this.diff;
+ }
+
if (!anyLineTooLong(diff)) {
this.syntaxLayer.init(diff);
waitForEventOnce(this, 'render').then(() => {
@@ -276,83 +675,21 @@
return diff;
}
- handleShouldScrollIntoViewChanged(shouldScrollIntoView?: boolean) {
- // Wait for comment to be rendered before scrolling to it
- if (shouldScrollIntoView) {
- const resizeObserver = new ResizeObserver(
- (_entries: ResizeObserverEntry[], observer: ResizeObserver) => {
- if (this.offsetHeight > 0) {
- queryAndAssert<HTMLDivElement>(this, '.comment-box').focus();
- this.scrollIntoView();
- }
- observer.unobserve(this);
- }
- );
- resizeObserver.observe(this);
+ private getDiffUrlForPath() {
+ if (!this.changeNum || !this.repoName || !this.thread?.path) {
+ return undefined;
}
+ if (this.isNewThread()) return undefined;
+ return GerritNav.getUrlForDiffById(
+ this.changeNum,
+ this.repoName,
+ this.thread.path,
+ this.thread.patchNum
+ );
}
- _shouldShowCommentContext(
- changeNum?: NumericChangeId,
- showCommentContext?: boolean,
- diff?: DiffInfo
- ) {
- return changeNum && showCommentContext && !!diff;
- }
-
- addOrEditDraft(lineNum?: LineNumber, rangeParam?: CommentRange) {
- const lastComment = this.comments[this.comments.length - 1] || {};
- if (isDraft(lastComment)) {
- const commentEl = this._commentElWithDraftID(
- lastComment.id || lastComment.__draftID
- );
- if (!commentEl) throw new Error('Failed to find draft.');
- commentEl.editing = true;
-
- // If the comment was collapsed, re-open it to make it clear which
- // actions are available.
- commentEl.collapsed = false;
- } else {
- const range = rangeParam
- ? rangeParam
- : lastComment
- ? lastComment.range
- : undefined;
- const unresolved = lastComment ? lastComment.unresolved : undefined;
- this.addDraft(lineNum, range, unresolved);
- }
- }
-
- addDraft(lineNum?: LineNumber, range?: CommentRange, unresolved?: boolean) {
- const draft = this._newDraft(lineNum, range);
- draft.__editing = true;
- draft.unresolved = unresolved === false ? unresolved : true;
- this.commentsService.addDraft(draft);
- }
-
- _getDiffUrlForPath(
- projectName?: RepoName,
- changeNum?: NumericChangeId,
- path?: string,
- patchNum?: PatchSetNum
- ) {
- if (!changeNum || !projectName || !path) return undefined;
- if (isDraft(this.comments[0])) {
- return GerritNav.getUrlForDiffById(
- changeNum,
- projectName,
- path,
- patchNum
- );
- }
- const id = this.comments[0].id;
- if (!id) throw new Error('A published comment is missing the id.');
- return GerritNav.getUrlForComment(changeNum, projectName, id);
- }
-
- /** The parameter is for triggering re-computation only. */
- getHighlightRange(_: unknown) {
- const comment = this.comments?.[0];
+ private computeHighlightRange() {
+ const comment = this.getFirstComment();
if (!comment) return undefined;
if (comment.range) return comment.range;
if (comment.line) {
@@ -366,413 +703,141 @@
return undefined;
}
- _initLayers(disableTokenHighlighting: boolean) {
- if (!disableTokenHighlighting) {
- this.layers.push(new TokenHighlightLayer(this));
+ // Does not work for patchset level comments
+ private getUrlForFileComment() {
+ if (!this.repoName || !this.changeNum || this.isNewThread()) {
+ return undefined;
}
- this.layers.push(this.syntaxLayer);
+ assertIsDefined(this.rootId, 'rootId of comment thread');
+ return GerritNav.getUrlForComment(
+ this.changeNum,
+ this.repoName,
+ this.rootId
+ );
}
- _getUrlForViewDiff(
- comments: UIComment[],
- changeNum?: NumericChangeId,
- projectName?: RepoName
- ): string {
- if (!changeNum) return '';
- if (!projectName) return '';
- check(comments.length > 0, 'comment not found');
- return GerritNav.getUrlForComment(changeNum, projectName, comments[0].id!);
- }
-
- _getDiffUrlForComment(
- projectName?: RepoName,
- changeNum?: NumericChangeId,
- path?: string,
- patchNum?: PatchSetNum
- ) {
- if (!projectName || !changeNum || !path) return undefined;
- if (
- (this.comments.length && this.comments[0].side === 'PARENT') ||
- isDraft(this.comments[0])
- ) {
- if (this.lineNum === 'LOST') throw new Error('invalid lineNum lost');
- return GerritNav.getUrlForDiffById(
- changeNum,
- projectName,
- path,
- patchNum,
- undefined,
- this.lineNum === FILE ? undefined : this.lineNum
- );
- }
- const id = this.comments[0].id;
- if (!id) throw new Error('A published comment is missing the id.');
- return GerritNav.getUrlForComment(changeNum, projectName, id);
- }
-
- handleCopyLink() {
+ private handleCopyLink() {
+ const comment = this.getFirstComment();
+ if (!comment) return;
assertIsDefined(this.changeNum, 'changeNum');
- assertIsDefined(this.projectName, 'projectName');
+ assertIsDefined(this.repoName, 'repoName');
const url = generateAbsoluteUrl(
GerritNav.getUrlForCommentsTab(
- this.changeNum,
- this.projectName,
- this.comments[0].id!
+ this.changeNum!,
+ this.repoName!,
+ comment.id
)
);
- navigator.clipboard.writeText(url).then(() => {
+ assertIsDefined(url, 'url for comment');
+ navigator.clipboard.writeText(generateAbsoluteUrl(url)).then(() => {
fireAlert(this, 'Link copied to clipboard');
});
}
- _isPatchsetLevelComment(path?: string) {
- return path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
+ private getDisplayPath() {
+ if (this.isPatchsetLevel()) return 'Patchset';
+ return computeDisplayPath(this.thread?.path);
}
- _computeShowPortedComment(comment: UIComment) {
- if (this._orderedComments.length === 0) return false;
- return this.showPortedComment && comment.id === this._orderedComments[0].id;
- }
-
- _computeDisplayPath(path?: string) {
- const displayPath = computeDisplayPath(path);
- if (displayPath === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
- return 'Patchset';
- }
- return displayPath;
- }
-
- _computeDisplayLine(lineNum?: LineNumber, range?: CommentRange) {
- if (lineNum === FILE) {
- if (this.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
- return '';
- }
- return FILE;
- }
- if (lineNum) return `#${lineNum}`;
+ private computeDisplayLine() {
+ assertIsDefined(this.thread, 'thread');
+ if (this.thread.line === FILE) return this.isPatchsetLevel() ? '' : FILE;
+ if (this.thread.line) return `#${this.thread.line}`;
// If range is set, then lineNum equals the end line of the range.
- if (range) return `#${range.end_line}`;
+ if (this.thread.range) return `#${this.thread.range.end_line}`;
return '';
}
- _getLoggedIn() {
- return this.restApiService.getLoggedIn();
+ private isRobotComment() {
+ return isRobot(this.getLastComment());
}
- _getUnresolvedLabel(unresolved?: boolean) {
- return unresolved ? 'Unresolved' : 'Resolved';
+ private getFirstComment() {
+ assertIsDefined(this.thread);
+ return getFirstComment(this.thread);
}
- @observe('comments.*')
- _commentsChanged() {
- this._orderedComments = sortComments(this.comments);
- this.updateThreadProperties();
+ private getLastComment() {
+ assertIsDefined(this.thread);
+ return getLastComment(this.thread);
}
- updateThreadProperties() {
- if (this._orderedComments.length) {
- this._lastComment = this._getLastComment();
- this.unresolved = this._lastComment.unresolved;
- this.hasDraft = isDraft(this._lastComment);
- this.isRobotComment = isRobot(this._lastComment);
+ private handleExpandShortcut() {
+ this.expandCollapseComments(false);
+ }
+
+ private handleCollapseShortcut() {
+ this.expandCollapseComments(true);
+ }
+
+ private expandCollapseComments(actionIsCollapse: boolean) {
+ for (const comment of this.commentElements ?? []) {
+ (comment as GrComment).collapsed = actionIsCollapse;
}
}
- _shouldDisableAction(_showActions?: boolean, _lastComment?: UIComment) {
- return !_showActions || !_lastComment || isDraft(_lastComment);
- }
-
- _hideActions(_showActions?: boolean, _lastComment?: UIComment) {
- return (
- this._shouldDisableAction(_showActions, _lastComment) ||
- isRobot(_lastComment)
- );
- }
-
- _getLastComment() {
- return this._orderedComments[this._orderedComments.length - 1] || {};
- }
-
- private handleExpandShortcut(e: KeyboardEvent) {
- if (this.shortcuts.shouldSuppress(e)) return;
- this._expandCollapseComments(false);
- }
-
- private handleCollapseShortcut(e: KeyboardEvent) {
- if (this.shortcuts.shouldSuppress(e)) return;
- this._expandCollapseComments(true);
- }
-
- _expandCollapseComments(actionIsCollapse: boolean) {
- const comments = this.root?.querySelectorAll('gr-comment');
- if (!comments) return;
- for (const comment of comments) {
- comment.collapsed = actionIsCollapse;
+ private async createReplyComment(
+ content: string,
+ userWantsToEdit: boolean,
+ unresolved: boolean
+ ) {
+ const replyingTo = this.getLastComment();
+ assertIsDefined(this.thread, 'thread');
+ assertIsDefined(replyingTo, 'the comment that the user wants to reply to');
+ if (isDraft(replyingTo)) {
+ throw new Error('cannot reply to draft');
}
- }
-
- /**
- * Sets the initial state of the comment thread.
- * Expands the thread if one of the following is true:
- * - last {UNRESOLVED_EXPAND_COUNT} comments expanded by default if the
- * thread is unresolved,
- * - it's a robot comment.
- * - it's a draft
- */
- _setInitialExpandedState() {
- if (this._orderedComments) {
- for (let i = 0; i < this._orderedComments.length; i++) {
- const comment = this._orderedComments[i];
- if (isDraft(comment)) {
- comment.collapsed = false;
- continue;
- }
- const isRobotComment = !!(comment as UIRobot).robot_id;
- // False if it's an unresolved comment under UNRESOLVED_EXPAND_COUNT.
- const resolvedThread =
- !this.unresolved ||
- this._orderedComments.length - i - 1 >= UNRESOLVED_EXPAND_COUNT;
- if (comment.collapsed === undefined) {
- comment.collapsed = !isRobotComment && resolvedThread;
- }
+ if (isUnsaved(replyingTo)) {
+ throw new Error('cannot reply to unsaved comment');
+ }
+ const unsaved = createUnsavedReply(replyingTo, content, unresolved);
+ if (userWantsToEdit) {
+ this.unsavedComment = unsaved;
+ } else {
+ try {
+ this.saving = true;
+ await this.getCommentsModel().saveDraft(unsaved);
+ } finally {
+ this.saving = false;
}
}
}
- _createReplyComment(
- content?: string,
- isEditing?: boolean,
- unresolved?: boolean
- ) {
- this.reporting.recordDraftInteraction();
- const id = this._orderedComments[this._orderedComments.length - 1].id;
- if (!id) throw new Error('Cannot reply to comment without id.');
- const reply = this._newReply(id, content, unresolved);
-
- if (isEditing) {
- reply.__editing = true;
- this.commentsService.addDraft(reply);
- } else {
- assertIsDefined(this.changeNum, 'changeNum');
- assertIsDefined(this.patchNum, 'patchNum');
- this.restApiService
- .saveDiffDraft(this.changeNum, this.patchNum, reply)
- .then(result => {
- if (!result.ok) {
- fireAlert(document, 'Unable to restore draft');
- return;
- }
- this.restApiService.getResponseObject(result).then(obj => {
- const resComment = obj as unknown as DraftInfo;
- resComment.patch_set = reply.patch_set;
- this.commentsService.addDraft(resComment);
- });
- });
- }
- }
-
- _isDraft(comment: UIComment) {
- return isDraft(comment);
- }
-
- _processCommentReply(quote?: boolean) {
- const comment = this._lastComment;
+ private handleCommentReply(quote: boolean) {
+ const comment = this.getLastComment();
if (!comment) throw new Error('Failed to find last comment.');
- let content = undefined;
+ let content = '';
if (quote) {
const msg = comment.message;
if (!msg) throw new Error('Quoting empty comment.');
content = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
}
- this._createReplyComment(content, true, comment.unresolved);
+ this.createReplyComment(content, true, comment.unresolved ?? true);
}
- _handleCommentReply() {
- this._processCommentReply();
+ private handleCommentAck() {
+ this.createReplyComment('Ack', false, false);
}
- _handleCommentQuote() {
- this._processCommentReply(true);
+ private handleCommentDone() {
+ this.createReplyComment('Done', false, false);
}
- _handleCommentAck() {
- this._createReplyComment('Ack', false, false);
- }
-
- _handleCommentDone() {
- this._createReplyComment('Done', false, false);
- }
-
- _handleCommentFix(e: CustomEvent) {
+ private handleCommentFix(e: CustomEvent) {
const comment = e.detail.comment;
const msg = comment.message;
const quoted = msg.replace(NEWLINE_PATTERN, '\n> ') as string;
const quoteStr = '> ' + quoted + '\n\n';
const response = quoteStr + 'Please fix.';
- this._createReplyComment(response, false, true);
+ this.createReplyComment(response, false, true);
}
- _commentElWithDraftID(id?: string): GrComment | null {
- if (!id) return null;
- const els = this.root?.querySelectorAll('gr-comment');
- if (!els) return null;
- for (const el of els) {
- const c = el.comment;
- if (isRobot(c)) continue;
- if (c?.id === id || (isDraft(c) && c?.__draftID === id)) return el;
- }
- return null;
- }
-
- _newReply(
- inReplyTo: UrlEncodedCommentId,
- message?: string,
- unresolved?: boolean
- ) {
- const d = this._newDraft();
- d.in_reply_to = inReplyTo;
- if (message !== undefined) {
- d.message = message;
- }
- if (unresolved !== undefined) {
- d.unresolved = unresolved;
- }
- return d;
- }
-
- _newDraft(lineNum?: LineNumber, range?: CommentRange) {
- const d: UIDraft = {
- __draft: true,
- __draftID: 'draft__' + Math.random().toString(36),
- __date: new Date(),
- };
- if (lineNum === 'LOST') throw new Error('invalid lineNum lost');
- // For replies, always use same meta info as root.
- if (this.comments && this.comments.length >= 1) {
- const rootComment = this.comments[0];
- if (rootComment.path !== undefined) d.path = rootComment.path;
- if (rootComment.patch_set !== undefined)
- d.patch_set = rootComment.patch_set;
- if (rootComment.side !== undefined) d.side = rootComment.side;
- if (rootComment.line !== undefined) d.line = rootComment.line;
- if (rootComment.range !== undefined) d.range = rootComment.range;
- if (rootComment.parent !== undefined) d.parent = rootComment.parent;
- } else {
- // Set meta info for root comment.
- d.path = this.path;
- d.patch_set = this.patchNum;
- d.side = this._getSide(this.isOnParent);
-
- if (lineNum && lineNum !== FILE) {
- d.line = lineNum;
- }
- if (range) {
- d.range = range;
- }
- if (this.parentIndex) {
- d.parent = this.parentIndex;
- }
- }
- return d;
- }
-
- _getSide(isOnParent: boolean): CommentSide {
- return isOnParent ? CommentSide.PARENT : CommentSide.REVISION;
- }
-
- _computeRootId(comments: PolymerDeepPropertyChange<UIComment[], unknown>) {
- // Keep the root ID even if the comment was removed, so that notification
- // to sync will know which thread to remove.
- if (!comments.base.length) {
- return this.rootId;
- }
- return computeId(comments.base[0]);
- }
-
- _handleCommentDiscard() {
- assertIsDefined(this.changeNum, 'changeNum');
- assertIsDefined(this.patchNum, 'patchNum');
- // Check to see if there are any other open comments getting edited and
- // set the local storage value to its message value.
- for (const changeComment of this.comments) {
- if (isDraft(changeComment) && changeComment.__editing) {
- const commentLocation: StorageLocation = {
- changeNum: this.changeNum,
- patchNum: this.patchNum,
- path: changeComment.path,
- line: changeComment.line,
- };
- this.storage.setDraftComment(
- commentLocation,
- changeComment.message ?? ''
- );
- }
- }
- }
-
- _handleCommentUpdate(e: CustomEvent) {
- const comment = e.detail.comment;
- const index = this._indexOf(comment, this.comments);
- if (index === -1) {
- // This should never happen: comment belongs to another thread.
- this.reporting.error(
- new Error(`Comment update for another comment thread: ${comment}`)
- );
- return;
- }
- this.set(['comments', index], comment);
- // Because of the way we pass these comment objects around by-ref, in
- // combination with the fact that Polymer does dirty checking in
- // observers, the this.set() call above will not cause a thread update in
- // some situations.
- this.updateThreadProperties();
- }
-
- _indexOf(comment: UIComment | undefined, arr: UIComment[]) {
- if (!comment) return -1;
- for (let i = 0; i < arr.length; i++) {
- const c = arr[i];
- if (
- (isDraft(c) && isDraft(comment) && c.__draftID === comment.__draftID) ||
- (c.id && c.id === comment.id)
- ) {
- return i;
- }
- }
- return -1;
- }
-
- /** 2nd parameter is for triggering re-computation only. */
- _computeHostClass(unresolved?: boolean, _?: unknown) {
- if (this.isRobotComment) {
- return 'robotComment';
- }
- return unresolved ? 'unresolved' : '';
- }
-
- /**
- * Load the project config when a project name has been provided.
- *
- * @param name The project name.
- */
- _projectNameChanged(name?: RepoName) {
- if (!name) {
- return;
- }
- this.restApiService.getProjectConfig(name).then(config => {
- this._projectConfig = config;
- });
- }
-
- _computeAriaHeading(_orderedComments: UIComment[]) {
- const firstComment = _orderedComments[0];
- const author = firstComment?.author ?? this._selfAccount;
- const lastComment = _orderedComments[_orderedComments.length - 1] || {};
- const status = [
- lastComment.unresolved ? 'Unresolved' : '',
- isDraft(lastComment) ? 'Draft' : '',
- ].join(' ');
- return `${status} Comment thread by ${getUserName(undefined, author)}`;
+ private computeAriaHeading() {
+ const author = this.getFirstComment()?.author ?? this.account;
+ const user = getUserName(undefined, author);
+ const unresolvedStatus = this.unresolved ? 'Unresolved ' : '';
+ const draftStatus = this.isDraftOrUnsaved() ? 'Draft ' : '';
+ return `${unresolvedStatus}${draftStatus}Comment thread by ${user}`;
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
deleted file mode 100644
index c3faaa5..0000000
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.ts
+++ /dev/null
@@ -1,264 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-a11y-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- :host {
- font-family: var(--font-family);
- font-size: var(--font-size-normal);
- font-weight: var(--font-weight-normal);
- line-height: var(--line-height-normal);
- /* Explicitly set the background color of the diff. We
- * cannot use the diff content type ab because of the skip chunk preceding
- * it, diff processor assumes the chunk of type skip/ab can be collapsed
- * and hides our diff behind context control buttons.
- * */
- --dark-add-highlight-color: var(--background-color-primary);
- }
- gr-button {
- margin-left: var(--spacing-m);
- }
- gr-comment {
- border-bottom: 1px solid var(--comment-separator-color);
- }
- #actions {
- margin-left: auto;
- padding: var(--spacing-s) var(--spacing-m);
- }
- .comment-box {
- width: 80ch;
- max-width: 100%;
- background-color: var(--comment-background-color);
- color: var(--comment-text-color);
- box-shadow: var(--elevation-level-2);
- border-radius: var(--border-radius);
- flex-shrink: 0;
- }
- #container {
- display: var(--gr-comment-thread-display, flex);
- align-items: flex-start;
- margin: 0 var(--spacing-s) var(--spacing-s);
- white-space: normal;
- /** This is required for firefox to continue the inheritance */
- -webkit-user-select: inherit;
- -moz-user-select: inherit;
- -ms-user-select: inherit;
- user-select: inherit;
- }
- .comment-box.unresolved {
- background-color: var(--unresolved-comment-background-color);
- }
- .comment-box.robotComment {
- background-color: var(--robot-comment-background-color);
- }
- #commentInfoContainer {
- display: flex;
- }
- #unresolvedLabel {
- font-family: var(--font-family);
- margin: auto 0;
- padding: var(--spacing-m);
- }
- .pathInfo {
- display: flex;
- align-items: baseline;
- justify-content: space-between;
- padding: 0 var(--spacing-s) var(--spacing-s);
- }
- .fileName {
- padding: var(--spacing-m) var(--spacing-s) var(--spacing-m);
- }
- @media only screen and (max-width: 1200px) {
- .diff-container {
- display: none;
- }
- }
- .diff-container {
- margin-left: var(--spacing-l);
- border: 1px solid var(--border-color);
- flex-grow: 1;
- flex-shrink: 1;
- max-width: 1200px;
- }
- .view-diff-button {
- margin: var(--spacing-s) var(--spacing-m);
- }
- .view-diff-container {
- border-top: 1px solid var(--border-color);
- background-color: var(--background-color-primary);
- }
-
- /* In saved state the "reply" and "quote" buttons are 28px height.
- * top:4px positions the 20px icon vertically centered.
- * Currently in draft state the "save" and "cancel" buttons are 20px
- * height, so the link icon does not need a top:4px in gr-comment_html.
- */
- .link-icon {
- position: relative;
- top: 4px;
- cursor: pointer;
- }
- .fileName gr-copy-clipboard {
- display: inline-block;
- visibility: hidden;
- vertical-align: top;
- --gr-button-padding: 0px;
- }
- .fileName:focus-within gr-copy-clipboard,
- .fileName:hover gr-copy-clipboard {
- visibility: visible;
- }
- </style>
-
- <template is="dom-if" if="[[showFilePath]]">
- <template is="dom-if" if="[[showFileName]]">
- <div class="fileName">
- <template is="dom-if" if="[[_isPatchsetLevelComment(path)]]">
- <span> [[_computeDisplayPath(path)]] </span>
- </template>
- <template is="dom-if" if="[[!_isPatchsetLevelComment(path)]]">
- <a
- href$="[[_getDiffUrlForPath(projectName, changeNum, path, patchNum)]]"
- >
- [[_computeDisplayPath(path)]]
- </a>
- <gr-copy-clipboard
- hideInput=""
- text="[[_computeDisplayPath(path)]]"
- ></gr-copy-clipboard>
- </template>
- </div>
- </template>
- <div class="pathInfo">
- <template is="dom-if" if="[[!_isPatchsetLevelComment(path)]]">
- <a
- href$="[[_getDiffUrlForComment(projectName, changeNum, path, patchNum)]]"
- >[[_computeDisplayLine(lineNum, range)]]</a
- >
- </template>
- </div>
- </template>
- <div id="container">
- <h3 class="assistive-tech-only">
- [[_computeAriaHeading(_orderedComments)]]
- </h3>
- <div
- class$="[[_computeHostClass(unresolved, isRobotComment)]] comment-box"
- tabindex="0"
- >
- <template
- id="commentList"
- is="dom-repeat"
- items="[[_orderedComments]]"
- as="comment"
- >
- <gr-comment
- comment="{{comment}}"
- comments="{{comments}}"
- robot-button-disabled="[[_shouldDisableAction(_showActions, _lastComment)]]"
- change-num="[[changeNum]]"
- project-name="[[projectName]]"
- patch-num="[[patchNum]]"
- draft="[[_isDraft(comment)]]"
- show-actions="[[_showActions]]"
- show-patchset="[[showPatchset]]"
- show-ported-comment="[[_computeShowPortedComment(comment)]]"
- side="[[comment.side]]"
- project-config="[[_projectConfig]]"
- on-create-fix-comment="_handleCommentFix"
- on-comment-discard="_handleCommentDiscard"
- on-copy-comment-link="handleCopyLink"
- ></gr-comment>
- </template>
- <div
- id="commentInfoContainer"
- hidden$="[[_hideActions(_showActions, _lastComment)]]"
- >
- <span id="unresolvedLabel">[[_getUnresolvedLabel(unresolved)]]</span>
- <div id="actions">
- <iron-icon
- class="link-icon"
- on-click="handleCopyLink"
- class="copy"
- title="Copy link to this comment"
- icon="gr-icons:link"
- role="button"
- tabindex="0"
- >
- </iron-icon>
- <gr-button
- id="replyBtn"
- link=""
- class="action reply"
- on-click="_handleCommentReply"
- >Reply</gr-button
- >
- <gr-button
- id="quoteBtn"
- link=""
- class="action quote"
- on-click="_handleCommentQuote"
- >Quote</gr-button
- >
- <template is="dom-if" if="[[unresolved]]">
- <gr-button
- id="ackBtn"
- link=""
- class="action ack"
- on-click="_handleCommentAck"
- >Ack</gr-button
- >
- <gr-button
- id="doneBtn"
- link=""
- class="action done"
- on-click="_handleCommentDone"
- >Done</gr-button
- >
- </template>
- </div>
- </div>
- </div>
- <template
- is="dom-if"
- if="[[_shouldShowCommentContext(changeNum, showCommentContext, _diff)]]"
- >
- <div class="diff-container">
- <gr-diff
- id="diff"
- change-num="[[changeNum]]"
- diff="[[_diff]]"
- layers="[[layers]]"
- path="[[path]]"
- prefs="[[_prefs]]"
- render-prefs="[[_renderPrefs]]"
- highlight-range="[[getHighlightRange(comments)]]"
- >
- </gr-diff>
- <div class="view-diff-container">
- <a href="[[_getUrlForViewDiff(comments, changeNum, projectName)]]">
- <gr-button link class="view-diff-button">View Diff</gr-button>
- </a>
- </div>
- </div>
- </template>
- </div>
-`;
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 595de34..9e3db21 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
@@ -14,936 +14,362 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
import '../../../test/common-test-setup-karma';
import './gr-comment-thread';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {SpecialFilePath, Side} from '../../../constants/constants';
-import {
- sortComments,
- UIComment,
- UIRobot,
- UIDraft,
-} from '../../../utils/comment-util';
+import {DraftInfo, sortComments} from '../../../utils/comment-util';
import {GrCommentThread} from './gr-comment-thread';
import {
- PatchSetNum,
NumericChangeId,
UrlEncodedCommentId,
Timestamp,
- RobotId,
- RobotRunId,
+ CommentInfo,
RepoName,
- ConfigInfo,
- EmailAddress,
} from '../../../types/common';
-import {GrComment} from '../gr-comment/gr-comment';
-import {LineNumber} from '../../diff/gr-diff/gr-diff-line';
-import {
- tap,
- pressAndReleaseKeyOn,
-} from '@polymer/iron-test-helpers/mock-interactions';
-import {html} from '@polymer/polymer/lib/utils/html-tag';
import {
mockPromise,
- stubComments,
- stubReporting,
+ queryAndAssert,
stubRestApi,
+ waitUntilCalled,
+ MockPromise,
} from '../../../test/test-utils';
-import {_testOnly_resetState} from '../../../services/comments/comments-model';
+import {
+ createAccountDetailWithId,
+ createThread,
+} from '../../../test/test-data-generators';
+import {tap} from '@polymer/iron-test-helpers/mock-interactions';
import {SinonStub} from 'sinon';
+import {waitUntil} from '@open-wc/testing-helpers';
const basicFixture = fixtureFromElement('gr-comment-thread');
-const withCommentFixture = fixtureFromElement('gr-comment-thread');
+const c1 = {
+ author: {name: 'Kermit'},
+ id: 'the-root' as UrlEncodedCommentId,
+ message: 'start the conversation',
+ updated: '2021-11-01 10:11:12.000000000' as Timestamp,
+};
+
+const c2 = {
+ author: {name: 'Ms Piggy'},
+ id: 'the-reply' as UrlEncodedCommentId,
+ message: 'keep it going',
+ updated: '2021-11-02 10:11:12.000000000' as Timestamp,
+ in_reply_to: 'the-root' as UrlEncodedCommentId,
+};
+
+const c3 = {
+ author: {name: 'Kermit'},
+ id: 'the-draft' as UrlEncodedCommentId,
+ message: 'stop it',
+ updated: '2021-11-03 10:11:12.000000000' as Timestamp,
+ in_reply_to: 'the-reply' as UrlEncodedCommentId,
+ __draft: true,
+};
+
+const commentWithContext = {
+ author: {name: 'Kermit'},
+ id: 'the-draft' as UrlEncodedCommentId,
+ message: 'just for context',
+ updated: '2021-11-03 10:11:12.000000000' as Timestamp,
+ line: 5,
+ context_lines: [
+ {line_number: 4, context_line: 'content of line 4'},
+ {line_number: 5, context_line: 'content of line 5'},
+ {line_number: 6, context_line: 'content of line 6'},
+ ],
+};
suite('gr-comment-thread tests', () => {
- suite('basic test', () => {
- let element: GrCommentThread;
-
- setup(() => {
- stubRestApi('getLoggedIn').returns(Promise.resolve(false));
- _testOnly_resetState();
- element = basicFixture.instantiate();
- element.patchNum = 3 as PatchSetNum;
- element.changeNum = 1 as NumericChangeId;
- flush();
- });
-
- test('renders without patchNum and changeNum', async () => {
- const fixture = fixtureFromTemplate(
- html`<gr-comment-thread show-file-path="" path="path/to/file"></gr-change-metadata>`
- );
- fixture.instantiate();
- await flush();
- });
-
- test('comments are sorted correctly', () => {
- const comments: UIComment[] = [
- {
- message: 'i like you, too',
- in_reply_to: 'sallys_confession' as UrlEncodedCommentId,
- __date: new Date('2015-12-25'),
- },
- {
- id: 'sallys_confession' as UrlEncodedCommentId,
- message: 'i like you, jack',
- updated: '2015-12-24 15:00:20.396000000' as Timestamp,
- },
- {
- id: 'sally_to_dr_finklestein' as UrlEncodedCommentId,
- message: 'i’m running away',
- updated: '2015-10-31 09:00:20.396000000' as Timestamp,
- },
- {
- id: 'sallys_defiance' as UrlEncodedCommentId,
- in_reply_to: 'sally_to_dr_finklestein' as UrlEncodedCommentId,
- message: 'i will poison you so i can get away',
- updated: '2015-10-31 15:00:20.396000000' as Timestamp,
- },
- {
- id: 'dr_finklesteins_response' as UrlEncodedCommentId,
- in_reply_to: 'sally_to_dr_finklestein' as UrlEncodedCommentId,
- message: 'no i will pull a thread and your arm will fall off',
- updated: '2015-10-31 11:00:20.396000000' as Timestamp,
- },
- {
- id: 'sallys_mission' as UrlEncodedCommentId,
- message: 'i have to find santa',
- updated: '2015-12-24 15:00:20.396000000' as Timestamp,
- },
- ];
- const results = sortComments(comments);
- assert.deepEqual(results, [
- {
- id: 'sally_to_dr_finklestein' as UrlEncodedCommentId,
- message: 'i’m running away',
- updated: '2015-10-31 09:00:20.396000000' as Timestamp,
- },
- {
- id: 'dr_finklesteins_response' as UrlEncodedCommentId,
- in_reply_to: 'sally_to_dr_finklestein' as UrlEncodedCommentId,
- message: 'no i will pull a thread and your arm will fall off',
- updated: '2015-10-31 11:00:20.396000000' as Timestamp,
- },
- {
- id: 'sallys_defiance' as UrlEncodedCommentId,
- in_reply_to: 'sally_to_dr_finklestein' as UrlEncodedCommentId,
- message: 'i will poison you so i can get away',
- updated: '2015-10-31 15:00:20.396000000' as Timestamp,
- },
- {
- id: 'sallys_confession' as UrlEncodedCommentId,
- message: 'i like you, jack',
- updated: '2015-12-24 15:00:20.396000000' as Timestamp,
- },
- {
- id: 'sallys_mission' as UrlEncodedCommentId,
- message: 'i have to find santa',
- updated: '2015-12-24 15:00:20.396000000' as Timestamp,
- },
- {
- message: 'i like you, too' as UrlEncodedCommentId,
- in_reply_to: 'sallys_confession' as UrlEncodedCommentId,
- __date: new Date('2015-12-25'),
- },
- ]);
- });
-
- test('addOrEditDraft w/ edit draft', () => {
- element.comments = [
- {
- id: 'jacks_reply' as UrlEncodedCommentId,
- message: 'i like you, too',
- in_reply_to: 'sallys_confession' as UrlEncodedCommentId,
- updated: '2015-12-25 15:00:20.396000000' as Timestamp,
- __draft: true,
- },
- ];
- const commentElStub = sinon
- .stub(element, '_commentElWithDraftID')
- .callsFake(() => new GrComment());
- const addDraftStub = sinon.stub(element, 'addDraft');
-
- element.addOrEditDraft(123);
-
- assert.isTrue(commentElStub.called);
- assert.isFalse(addDraftStub.called);
- });
-
- test('addOrEditDraft w/o edit draft', () => {
- element.comments = [];
- const commentElStub = sinon
- .stub(element, '_commentElWithDraftID')
- .callsFake(() => new GrComment());
- const addDraftStub = sinon.stub(element, 'addDraft');
-
- element.addOrEditDraft(123);
-
- assert.isFalse(commentElStub.called);
- assert.isTrue(addDraftStub.called);
- });
-
- test('_shouldDisableAction', () => {
- let showActions = true;
- const lastComment: UIComment = {};
- assert.equal(
- element._shouldDisableAction(showActions, lastComment),
- false
- );
- showActions = false;
- assert.equal(
- element._shouldDisableAction(showActions, lastComment),
- true
- );
- showActions = true;
- lastComment.__draft = true;
- assert.equal(
- element._shouldDisableAction(showActions, lastComment),
- true
- );
- const robotComment: UIRobot = {
- id: '1234' as UrlEncodedCommentId,
- updated: '1234' as Timestamp,
- robot_id: 'robot_id' as RobotId,
- robot_run_id: 'robot_run_id' as RobotRunId,
- properties: {},
- fix_suggestions: [],
- };
- assert.equal(
- element._shouldDisableAction(showActions, robotComment),
- false
- );
- });
-
- test('_hideActions', () => {
- let showActions = true;
- const lastComment: UIComment = {};
- assert.equal(element._hideActions(showActions, lastComment), false);
- showActions = false;
- assert.equal(element._hideActions(showActions, lastComment), true);
- showActions = true;
- lastComment.__draft = true;
- assert.equal(element._hideActions(showActions, lastComment), true);
- const robotComment: UIRobot = {
- id: '1234' as UrlEncodedCommentId,
- updated: '1234' as Timestamp,
- robot_id: 'robot_id' as RobotId,
- robot_run_id: 'robot_run_id' as RobotRunId,
- properties: {},
- fix_suggestions: [],
- };
- assert.equal(element._hideActions(showActions, robotComment), true);
- });
-
- test('setting project name loads the project config', async () => {
- const projectName = 'foo/bar/baz' as RepoName;
- const getProjectStub = stubRestApi('getProjectConfig').returns(
- Promise.resolve({} as ConfigInfo)
- );
- element.projectName = projectName;
- await flush();
- assert.isTrue(getProjectStub.calledWithExactly(projectName as never));
- });
-
- test('optionally show file path', () => {
- // Path info doesn't exist when showFilePath is false. Because it's in a
- // dom-if it is not yet in the dom.
- assert.isNotOk(element.shadowRoot?.querySelector('.pathInfo'));
-
- const commentStub = sinon.stub(GerritNav, 'getUrlForComment');
- element.changeNum = 123 as NumericChangeId;
- element.projectName = 'test project' as RepoName;
- element.path = 'path/to/file';
- element.patchNum = 3 as PatchSetNum;
- element.lineNum = 5;
- element.comments = [{id: 'comment_id' as UrlEncodedCommentId}];
- element.showFilePath = true;
- flush();
- assert.isOk(element.shadowRoot?.querySelector('.pathInfo'));
- assert.notEqual(
- getComputedStyle(element.shadowRoot!.querySelector('.pathInfo')!)
- .display,
- 'none'
- );
- assert.isTrue(
- commentStub.calledWithExactly(
- element.changeNum,
- element.projectName,
- 'comment_id' as UrlEncodedCommentId
- )
- );
- });
-
- test('_computeDisplayPath', () => {
- let path = 'path/to/file';
- assert.equal(element._computeDisplayPath(path), 'path/to/file');
-
- element.lineNum = 5;
- assert.equal(element._computeDisplayPath(path), 'path/to/file');
-
- element.patchNum = 3 as PatchSetNum;
- path = SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
- assert.equal(element._computeDisplayPath(path), 'Patchset');
- });
-
- test('_computeDisplayLine', () => {
- element.lineNum = 5;
- assert.equal(
- element._computeDisplayLine(element.lineNum, element.range),
- '#5'
- );
-
- element.path = SpecialFilePath.COMMIT_MESSAGE;
- element.lineNum = 5;
- assert.equal(
- element._computeDisplayLine(element.lineNum, element.range),
- '#5'
- );
-
- element.lineNum = undefined;
- element.path = SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
- assert.equal(
- element._computeDisplayLine(element.lineNum, element.range),
- ''
- );
- });
- });
-});
-
-suite('comment action tests with unresolved thread', () => {
- let element: GrCommentThread;
- let addDraftServiceStub: SinonStub;
- let saveDiffDraftStub: SinonStub;
- let comment = {
- id: '7afa4931_de3d65bd',
- path: '/path/to/file.txt',
- line: 5,
- in_reply_to: 'baf0414d_60047215' as UrlEncodedCommentId,
- updated: '2015-12-21 02:01:10.850000000',
- message: 'Done',
- };
- const peanutButterComment = {
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com' as EmailAddress as EmailAddress,
- },
- id: 'baf0414d_60047215' as UrlEncodedCommentId,
- line: 5,
- in_reply_to: 'baf0414d_60047215' as UrlEncodedCommentId,
- message: 'is this a crossover episode!?',
- updated: '2015-12-08 19:48:33.843000000' as Timestamp,
- path: '/path/to/file.txt',
- unresolved: true,
- patch_set: 3 as PatchSetNum,
- };
- const mockResponse: Response = {
- ...new Response(),
- headers: {} as Headers,
- redirected: false,
- status: 200,
- statusText: '',
- type: '' as ResponseType,
- url: '',
- ok: true,
- text() {
- return Promise.resolve(")]}'\n" + JSON.stringify(comment));
- },
- };
- let saveDiffDraftPromiseResolver: (value?: Response) => void;
- setup(() => {
- addDraftServiceStub = stubComments('addDraft');
- stubRestApi('getLoggedIn').returns(Promise.resolve(false));
- saveDiffDraftStub = stubRestApi('saveDiffDraft').returns(
- new Promise<Response>(
- resolve =>
- (saveDiffDraftPromiseResolver = resolve as (value?: Response) => void)
- )
- );
- stubRestApi('deleteDiffDraft').returns(
- Promise.resolve({...new Response(), ok: true})
- );
- element = withCommentFixture.instantiate();
- element.patchNum = 1 as PatchSetNum;
- element.changeNum = 1 as NumericChangeId;
- element.comments = [peanutButterComment];
- flush();
- });
-
- test('reply', () => {
- saveDiffDraftPromiseResolver(mockResponse);
-
- const commentEl = element.shadowRoot?.querySelector('gr-comment');
- const reportStub = stubReporting('recordDraftInteraction');
- assert.ok(commentEl);
-
- const replyBtn = element.$.replyBtn;
- tap(replyBtn);
- flush();
- const draft = addDraftServiceStub.firstCall.args[0];
- assert.isOk(draft);
- assert.notOk(draft.message, 'message should be empty');
- assert.equal(
- draft.in_reply_to,
- 'baf0414d_60047215' as UrlEncodedCommentId as UrlEncodedCommentId
- );
- assert.isTrue(reportStub.calledOnce);
- });
-
- test('quote reply', () => {
- saveDiffDraftPromiseResolver(mockResponse);
-
- const commentEl = element.shadowRoot?.querySelector('gr-comment');
- const reportStub = stubReporting('recordDraftInteraction');
- assert.ok(commentEl);
-
- const quoteBtn = element.$.quoteBtn;
- tap(quoteBtn);
- flush();
-
- const draft = addDraftServiceStub.firstCall.args[0];
- // the quote reply is not autmatically saved so verify that id is not set
- assert.isNotOk(draft.id);
- // verify that the draft returned was not saved
- assert.isNotOk(saveDiffDraftStub.called);
- assert.equal(draft.message, '> is this a crossover episode!?\n\n');
- assert.equal(
- draft.in_reply_to,
- 'baf0414d_60047215' as UrlEncodedCommentId as UrlEncodedCommentId
- );
- assert.isTrue(reportStub.calledOnce);
- });
-
- test('quote reply multiline', () => {
- saveDiffDraftPromiseResolver(mockResponse);
- const reportStub = stubReporting('recordDraftInteraction');
- element.comments = [
- {
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com' as EmailAddress as EmailAddress,
- },
- id: 'baf0414d_60047215' as UrlEncodedCommentId,
- path: 'test',
- line: 5,
- message: 'is this a crossover episode!?\nIt might be!',
- updated: '2015-12-08 19:48:33.843000000' as Timestamp,
- },
- ];
- flush();
-
- const commentEl = element.shadowRoot?.querySelector('gr-comment');
- assert.ok(commentEl);
-
- const quoteBtn = element.$.quoteBtn;
- tap(quoteBtn);
- flush();
-
- const draft = addDraftServiceStub.firstCall.args[0];
- assert.equal(
- draft.message,
- '> is this a crossover episode!?\n> It might be!\n\n'
- );
- assert.equal(draft.in_reply_to, 'baf0414d_60047215' as UrlEncodedCommentId);
- assert.isTrue(reportStub.calledOnce);
- });
-
- test('ack', async () => {
- saveDiffDraftPromiseResolver(mockResponse);
- comment = {
- id: '7afa4931_de3d65bd',
- path: '/path/to/file.txt',
- line: 5,
- in_reply_to: 'baf0414d_60047215' as UrlEncodedCommentId,
- updated: '2015-12-21 02:01:10.850000000',
- message: 'Ack',
- };
- const reportStub = stubReporting('recordDraftInteraction');
- element.changeNum = 42 as NumericChangeId;
- element.patchNum = 1 as PatchSetNum;
-
- const commentEl = element.shadowRoot?.querySelector('gr-comment');
- assert.ok(commentEl);
-
- const ackBtn = element.shadowRoot?.querySelector('#ackBtn');
- assert.isOk(ackBtn);
- tap(ackBtn!);
- await flush();
- const draft = addDraftServiceStub.firstCall.args[0];
- assert.equal(draft.message, 'Ack');
- assert.equal(draft.in_reply_to, 'baf0414d_60047215' as UrlEncodedCommentId);
- assert.isNotOk(draft.unresolved);
- assert.isTrue(reportStub.calledOnce);
- });
-
- test('done', async () => {
- saveDiffDraftPromiseResolver(mockResponse);
- comment = {
- id: '7afa4931_de3d65bd',
- path: '/path/to/file.txt',
- line: 5,
- in_reply_to: 'baf0414d_60047215' as UrlEncodedCommentId,
- updated: '2015-12-21 02:01:10.850000000',
- message: 'Done',
- };
- const reportStub = stubReporting('recordDraftInteraction');
- assert.isFalse(saveDiffDraftStub.called);
- element.changeNum = 42 as NumericChangeId;
- element.patchNum = 1 as PatchSetNum;
- const commentEl = element.shadowRoot?.querySelector('gr-comment');
- assert.ok(commentEl);
-
- const doneBtn = element.shadowRoot?.querySelector('#doneBtn');
- assert.isOk(doneBtn);
- tap(doneBtn!);
- await flush();
- const draft = addDraftServiceStub.firstCall.args[0];
- // Since the reply is automatically saved, verify that draft.id is set in
- // the model
- assert.equal(draft.id, '7afa4931_de3d65bd');
- assert.equal(draft.message, 'Done');
- assert.equal(draft.in_reply_to, 'baf0414d_60047215' as UrlEncodedCommentId);
- assert.isNotOk(draft.unresolved);
- assert.isTrue(reportStub.calledOnce);
- assert.isTrue(saveDiffDraftStub.called);
- });
-
- test('save', async () => {
- saveDiffDraftPromiseResolver(mockResponse);
- element.changeNum = 42 as NumericChangeId;
- element.patchNum = 1 as PatchSetNum;
- element.path = '/path/to/file.txt';
- const commentEl = element.shadowRoot?.querySelector('gr-comment');
- assert.ok(commentEl);
-
- element.shadowRoot?.querySelector('gr-comment')?._fireSave();
-
- await flush();
- assert.equal(element.rootId, 'baf0414d_60047215' as UrlEncodedCommentId);
- });
-
- test('please fix', async () => {
- comment = peanutButterComment;
- element.changeNum = 42 as NumericChangeId;
- element.patchNum = 1 as PatchSetNum;
- const commentEl = element.shadowRoot?.querySelector('gr-comment');
- assert.ok(commentEl);
- const promise = mockPromise();
- commentEl!.addEventListener('create-fix-comment', async () => {
- assert.isTrue(saveDiffDraftStub.called);
- assert.isFalse(addDraftServiceStub.called);
- saveDiffDraftPromiseResolver(mockResponse);
- // flushing so the saveDiffDraftStub resolves and the draft is returned
- await flush();
- assert.isTrue(saveDiffDraftStub.called);
- assert.isTrue(addDraftServiceStub.called);
- const draft = saveDiffDraftStub.firstCall.args[2];
- assert.equal(
- draft.message,
- '> is this a crossover episode!?\n\nPlease fix.'
- );
- assert.equal(
- draft.in_reply_to,
- 'baf0414d_60047215' as UrlEncodedCommentId
- );
- assert.isTrue(draft.unresolved);
- promise.resolve();
- });
- assert.isFalse(saveDiffDraftStub.called);
- assert.isFalse(addDraftServiceStub.called);
- commentEl!.dispatchEvent(
- new CustomEvent('create-fix-comment', {
- detail: {comment: commentEl!.comment},
- composed: true,
- bubbles: false,
- })
- );
- await promise;
- });
-
- test('discard', async () => {
- element.changeNum = 42 as NumericChangeId;
- element.patchNum = 1 as PatchSetNum;
- element.path = '/path/to/file.txt';
- assert.isOk(element.comments[0]);
- const deleteDraftStub = stubComments('deleteDraft');
- element.push(
- 'comments',
- element._newReply(
- element.comments[0]!.id as UrlEncodedCommentId,
- 'it’s pronouced jiff, not giff'
- )
- );
- await flush();
-
- const draftEl = element.root?.querySelectorAll('gr-comment')[1];
- assert.ok(draftEl);
- draftEl?._fireSave(); // tell the model about the draft
- const promise = mockPromise();
- draftEl!.addEventListener('comment-discard', () => {
- assert.isTrue(deleteDraftStub.called);
- promise.resolve();
- });
- draftEl!._fireDiscard();
- await promise;
- });
-
- test('discard with a single comment still fires event with previous rootId', async () => {
- element.changeNum = 42 as NumericChangeId;
- element.patchNum = 1 as PatchSetNum;
- element.path = '/path/to/file.txt';
- element.comments = [];
- element.addOrEditDraft(1 as LineNumber);
- const draft = addDraftServiceStub.firstCall.args[0];
- element.comments = [draft];
- flush();
- const rootId = element.rootId;
- assert.isOk(rootId);
- flush();
- const draftEl = element.root?.querySelectorAll('gr-comment')[0];
- assert.ok(draftEl);
- const deleteDraftStub = stubComments('deleteDraft');
- const promise = mockPromise();
- draftEl!.addEventListener('comment-discard', () => {
- assert.isTrue(deleteDraftStub.called);
- promise.resolve();
- });
- draftEl!._fireDiscard();
- await promise;
- assert.isTrue(deleteDraftStub.called);
- });
-
- test('comment-update', () => {
- const commentEl = element.shadowRoot?.querySelector('gr-comment');
- const updatedComment = {
- id: element.comments[0].id,
- foo: 'bar',
- };
- assert.isOk(commentEl);
- commentEl!.dispatchEvent(
- new CustomEvent('comment-update', {
- detail: {comment: updatedComment},
- composed: true,
- bubbles: true,
- })
- );
- assert.strictEqual(element.comments[0], updatedComment);
- });
-
- suite('jack and sally comment data test consolidation', () => {
- setup(() => {
- element.comments = [
- {
- id: 'jacks_reply' as UrlEncodedCommentId,
- message: 'i like you, too',
- in_reply_to: 'sallys_confession' as UrlEncodedCommentId,
- updated: '2015-12-25 15:00:20.396000000' as Timestamp,
- path: 'abcd',
- unresolved: false,
- },
- {
- id: 'sallys_confession' as UrlEncodedCommentId,
- in_reply_to: 'nonexistent_comment' as UrlEncodedCommentId,
- message: 'i like you, jack',
- updated: '2015-12-24 15:00:20.396000000' as Timestamp,
- path: 'abcd',
- },
- {
- id: 'sally_to_dr_finklestein' as UrlEncodedCommentId,
- in_reply_to: 'nonexistent_comment' as UrlEncodedCommentId,
- message: 'i’m running away',
- updated: '2015-10-31 09:00:20.396000000' as Timestamp,
- path: 'abcd',
- },
- {
- id: 'sallys_defiance' as UrlEncodedCommentId,
- message: 'i will poison you so i can get away',
- updated: '2015-10-31 15:00:20.396000000' as Timestamp,
- path: 'abcd',
- },
- ];
- });
-
- test('orphan replies', () => {
- assert.equal(4, element._orderedComments.length);
- });
-
- test('keyboard shortcuts', () => {
- const expandCollapseStub = sinon.stub(element, '_expandCollapseComments');
- pressAndReleaseKeyOn(element, 69, null, 'e');
- assert.isTrue(expandCollapseStub.lastCall.calledWith(false));
-
- pressAndReleaseKeyOn(element, 69, 'shift', 'E');
- assert.isTrue(expandCollapseStub.lastCall.calledWith(true));
- });
-
- test('comment in_reply_to is either null or most recent comment', () => {
- element._createReplyComment('dummy', true);
- const draft = addDraftServiceStub.firstCall.args[0];
- element.comments = [...element.comments, draft];
- flush();
- assert.equal(element._orderedComments.length, 5);
- assert.equal(
- element._orderedComments[4].in_reply_to,
- 'jacks_reply' as UrlEncodedCommentId
- );
- });
-
- test('resolvable comments', () => {
- assert.isFalse(element.unresolved);
- element._createReplyComment('dummy', true, true);
- const draft = addDraftServiceStub.firstCall.args[0];
- element.comments = [...element.comments, draft];
- flush();
- assert.isTrue(element.unresolved);
- });
-
- test('_setInitialExpandedState with unresolved', () => {
- element.unresolved = true;
- element._setInitialExpandedState();
- for (let i = 0; i < element.comments.length; i++) {
- assert.isFalse(element.comments[i].collapsed);
- }
- });
-
- test('_setInitialExpandedState without unresolved', () => {
- element.unresolved = false;
- element._setInitialExpandedState();
- for (let i = 0; i < element.comments.length; i++) {
- assert.isTrue(element.comments[i].collapsed);
- }
- });
-
- test('_setInitialExpandedState with robot_ids', () => {
- for (let i = 0; i < element.comments.length; i++) {
- (element.comments[i] as UIRobot).robot_id = '123' as RobotId;
- }
- element._setInitialExpandedState();
- for (let i = 0; i < element.comments.length; i++) {
- assert.isFalse(element.comments[i].collapsed);
- }
- });
-
- test('_setInitialExpandedState with collapsed state', () => {
- element.comments[0].collapsed = false;
- element.unresolved = false;
- element._setInitialExpandedState();
- assert.isFalse(element.comments[0].collapsed);
- for (let i = 1; i < element.comments.length; i++) {
- assert.isTrue(element.comments[i].collapsed);
- }
- });
- });
-
- test('_computeHostClass', () => {
- assert.equal(element._computeHostClass(true), 'unresolved');
- assert.equal(element._computeHostClass(false), '');
- });
-
- test('addDraft sets unresolved state correctly', () => {
- let unresolved = true;
- let draft;
- element.comments = [];
- element.path = 'abcd';
- element.addDraft(undefined, undefined, unresolved);
- draft = addDraftServiceStub.lastCall.args[0];
- assert.equal(draft.unresolved, true);
-
- unresolved = false; // comment should get added as actually resolved.
- element.comments = [];
- element.addDraft(undefined, undefined, unresolved);
- draft = addDraftServiceStub.lastCall.args[0];
- assert.equal(draft.unresolved, false);
-
- element.comments = [];
- element.addDraft();
- draft = addDraftServiceStub.lastCall.args[0];
- assert.equal(draft.unresolved, true);
- });
-
- test('_newDraft with root', () => {
- const draft = element._newDraft();
- assert.equal(draft.patch_set, 3 as PatchSetNum);
- });
-
- test('_newDraft with no root', () => {
- element.comments = [];
- element.diffSide = Side.RIGHT;
- element.patchNum = 2 as PatchSetNum;
- const draft = element._newDraft();
- assert.equal(draft.patch_set, 2 as PatchSetNum);
- });
-
- test('new comment gets created', () => {
- element.comments = [];
- element.path = 'abcd';
- element.addOrEditDraft(1);
- const draft = addDraftServiceStub.firstCall.args[0];
- element.comments = [draft];
- flush();
- assert.equal(element.comments.length, 1);
- // Mock a submitted comment.
- element.comments[0].id = (element.comments[0] as UIDraft)
- .__draftID as UrlEncodedCommentId;
- delete (element.comments[0] as UIDraft).__draft;
- element.addOrEditDraft(1);
- assert.equal(addDraftServiceStub.callCount, 2);
- });
-
- test('unresolved label', () => {
- element.unresolved = false;
- const label = element.shadowRoot?.querySelector('#unresolvedLabel');
- assert.isOk(label);
- assert.isFalse(label!.hasAttribute('hidden'));
- element.unresolved = true;
- assert.isFalse(label!.hasAttribute('hidden'));
- });
-
- test('draft comments are at the end of orderedComments', () => {
- element.comments = [
- {
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com' as EmailAddress,
- },
- id: '2' as UrlEncodedCommentId,
- line: 5,
- message: 'Earlier draft',
- updated: '2015-12-08 19:48:33.843000000' as Timestamp,
- __draft: true,
- },
- {
- author: {
- name: 'Mr. Peanutbutter2',
- email: 'tenn1sballchaser@aol.com' as EmailAddress,
- },
- id: '1' as UrlEncodedCommentId,
- line: 5,
- message: 'This comment was left last but is not a draft',
- updated: '2015-12-10 19:48:33.843000000' as Timestamp,
- },
- {
- author: {
- name: 'Mr. Peanutbutter2',
- email: 'tenn1sballchaser@aol.com' as EmailAddress,
- },
- id: '3' as UrlEncodedCommentId,
- line: 5,
- message: 'Later draft',
- updated: '2015-12-09 19:48:33.843000000' as Timestamp,
- __draft: true,
- },
- ];
- assert.equal(element._orderedComments[0].id, '1' as UrlEncodedCommentId);
- assert.equal(element._orderedComments[1].id, '2' as UrlEncodedCommentId);
- assert.equal(element._orderedComments[2].id, '3' as UrlEncodedCommentId);
- });
-
- test('reflects lineNum and commentSide to attributes', () => {
- element.lineNum = 7;
- element.diffSide = Side.LEFT;
-
- assert.equal(element.getAttribute('line-num'), '7');
- assert.equal(element.getAttribute('diff-side'), Side.LEFT);
- });
-
- test('reflects range to JSON serialized attribute if set', () => {
- element.range = {
- start_line: 4,
- end_line: 5,
- start_character: 6,
- end_character: 7,
- };
-
- assert.isOk(element.getAttribute('range'));
- assert.deepEqual(JSON.parse(element.getAttribute('range')!), {
- start_line: 4,
- end_line: 5,
- start_character: 6,
- end_character: 7,
- });
- });
-
- test('removes range attribute if range is unset', () => {
- element.range = {
- start_line: 4,
- end_line: 5,
- start_character: 6,
- end_character: 7,
- };
- element.range = undefined;
-
- assert.notOk(element.hasAttribute('range'));
- });
-});
-
-suite('comment action tests on resolved comments', () => {
let element: GrCommentThread;
- setup(() => {
+ setup(async () => {
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
- stubRestApi('saveDiffDraft').returns(
- Promise.resolve({
- ...new Response(),
- ok: true,
- text() {
- return Promise.resolve(
- ")]}'\n" +
- JSON.stringify({
- id: '7afa4931_de3d65bd',
- path: '/path/to/file.txt',
- line: 5,
- in_reply_to: 'baf0414d_60047215' as UrlEncodedCommentId,
- updated: '2015-12-21 02:01:10.850000000',
- message: 'Done',
- })
- );
- },
- })
- );
- stubRestApi('deleteDiffDraft').returns(
- Promise.resolve({...new Response(), ok: true})
- );
- element = withCommentFixture.instantiate();
- element.patchNum = 1 as PatchSetNum;
+ element = basicFixture.instantiate();
element.changeNum = 1 as NumericChangeId;
- element.comments = [
+ element.showFileName = true;
+ element.showFilePath = true;
+ element.repoName = 'test-repo-name' as RepoName;
+ await element.updateComplete;
+ element.account = {...createAccountDetailWithId(13), name: 'Yoda'};
+ });
+
+ test('renders with draft', async () => {
+ element.thread = createThread(c1, c2, c3);
+ await element.updateComplete;
+ });
+
+ test('renders with draft', async () => {
+ element.thread = createThread(c1, c2, c3);
+ await element.updateComplete;
+ expect(element).shadowDom.to.equal(`
+ <div class="fileName">
+ <span>test-path-comment-thread</span>
+ <gr-copy-clipboard hideinput=""></gr-copy-clipboard>
+ </div>
+ <div class="pathInfo">
+ <span>#314</span>
+ </div>
+ <div id="container">
+ <h3 class="assistive-tech-only">Draft Comment thread by Kermit</h3>
+ <div class="comment-box" tabindex="0">
+ <gr-comment collapsed="" initially-collapsed="" robot-button-disabled="" show-patchset=""></gr-comment>
+ <gr-comment collapsed="" initially-collapsed="" robot-button-disabled="" show-patchset=""></gr-comment>
+ <gr-comment robot-button-disabled="" show-patchset=""></gr-comment>
+ </div>
+ </div>
+ `);
+ });
+
+ test('renders unsaved', async () => {
+ element.thread = createThread();
+ await element.updateComplete;
+ expect(element).shadowDom.to.equal(`
+ <div class="fileName">
+ <span>test-path-comment-thread</span>
+ <gr-copy-clipboard hideinput=""></gr-copy-clipboard>
+ </div>
+ <div class="pathInfo">
+ <span>#314</span>
+ </div>
+ <div id="container">
+ <h3 class="assistive-tech-only">Unresolved Draft Comment thread by Yoda</h3>
+ <div class="comment-box unresolved" tabindex="0">
+ <gr-comment robot-button-disabled="" show-patchset=""></gr-comment>
+ </div>
+ </div>
+ `);
+ });
+
+ test('renders with actions resolved', async () => {
+ element.thread = createThread(c1, c2);
+ await element.updateComplete;
+ expect(queryAndAssert(element, '#container')).dom.to.equal(`
+ <div id="container">
+ <h3 class="assistive-tech-only">Comment thread by Kermit</h3>
+ <div class="comment-box" tabindex="0">
+ <gr-comment collapsed="" initially-collapsed="" show-patchset=""></gr-comment>
+ <gr-comment collapsed="" initially-collapsed="" show-patchset=""></gr-comment>
+ <div id="actionsContainer">
+ <span id="unresolvedLabel">
+ Resolved
+ </span>
+ <div id="actions">
+ <iron-icon class="copy link-icon" icon="gr-icons:link" role="button" tabindex="0" title="Copy link to this comment">
+ </iron-icon>
+ <gr-button aria-disabled="false" class="action reply" id="replyBtn" link="" role="button" tabindex="0">
+ Reply
+ </gr-button>
+ <gr-button aria-disabled="false" class="action quote" id="quoteBtn" link="" role="button" tabindex="0">
+ Quote
+ </gr-button>
+ </div>
+ </div>
+ </div>
+ </div>
+ `);
+ });
+
+ test('renders with actions unresolved', async () => {
+ element.thread = createThread(c1, {...c2, unresolved: true});
+ await element.updateComplete;
+ expect(queryAndAssert(element, '#container')).dom.to.equal(`
+ <div id="container">
+ <h3 class="assistive-tech-only">Unresolved Comment thread by Kermit</h3>
+ <div class="comment-box unresolved" tabindex="0">
+ <gr-comment show-patchset=""></gr-comment>
+ <gr-comment show-patchset=""></gr-comment>
+ <div id="actionsContainer">
+ <span id="unresolvedLabel">
+ Unresolved
+ </span>
+ <div id="actions">
+ <iron-icon class="copy link-icon" icon="gr-icons:link" role="button" tabindex="0" title="Copy link to this comment">
+ </iron-icon>
+ <gr-button aria-disabled="false" class="action reply" id="replyBtn" link="" role="button" tabindex="0">
+ Reply
+ </gr-button>
+ <gr-button aria-disabled="false" class="action quote" id="quoteBtn" link="" role="button" tabindex="0">
+ Quote
+ </gr-button>
+ <gr-button aria-disabled="false" class="action ack" id="ackBtn" link="" role="button" tabindex="0">
+ Ack
+ </gr-button>
+ <gr-button aria-disabled="false" class="action done" id="doneBtn" link="" role="button" tabindex="0">
+ Done
+ </gr-button>
+ </div>
+ </div>
+ </div>
+ </div>
+ `);
+ });
+
+ test('renders with diff', async () => {
+ element.showCommentContext = true;
+ element.thread = createThread(commentWithContext);
+ await element.updateComplete;
+ expect(queryAndAssert(element, '.diff-container')).dom.to.equal(`
+ <div class="diff-container">
+ <gr-diff
+ class="disable-context-control-buttons hide-line-length-indicator no-left"
+ id="diff"
+ style="--line-limit-marker:100ch; --content-width:none; --diff-max-width:none; --font-size:12px;"
+ >
+ </gr-diff>
+ <div class="view-diff-container">
+ <a href="">
+ <gr-button aria-disabled="false" class="view-diff-button" link="" role="button" tabindex="0">
+ View Diff
+ </gr-button>
+ </a>
+ </div>
+ </div>
+ `);
+ });
+
+ suite('action button clicks', () => {
+ let savePromise: MockPromise<DraftInfo>;
+ let stub: SinonStub;
+
+ setup(async () => {
+ savePromise = mockPromise<DraftInfo>();
+ stub = sinon
+ .stub(element.getCommentsModel(), 'saveDraft')
+ .returns(savePromise);
+
+ element.thread = createThread(c1, {...c2, unresolved: true});
+ await element.updateComplete;
+ });
+
+ test('handle Ack', async () => {
+ tap(queryAndAssert(element, '#ackBtn'));
+ waitUntilCalled(stub, 'saveDraft()');
+ assert.equal(stub.lastCall.firstArg.message, 'Ack');
+ assert.equal(stub.lastCall.firstArg.unresolved, false);
+ assert.isTrue(element.saving);
+
+ savePromise.resolve();
+ await element.updateComplete;
+ assert.isFalse(element.saving);
+ });
+
+ test('handle Done', async () => {
+ tap(queryAndAssert(element, '#doneBtn'));
+ waitUntilCalled(stub, 'saveDraft()');
+ assert.equal(stub.lastCall.firstArg.message, 'Done');
+ assert.equal(stub.lastCall.firstArg.unresolved, false);
+ });
+
+ test('handle Reply', async () => {
+ assert.isUndefined(element.unsavedComment);
+ tap(queryAndAssert(element, '#replyBtn'));
+ assert.equal(element.unsavedComment?.message, '');
+ });
+
+ test('handle Quote', async () => {
+ assert.isUndefined(element.unsavedComment);
+ tap(queryAndAssert(element, '#quoteBtn'));
+ assert.equal(element.unsavedComment?.message?.trim(), `> ${c2.message}`);
+ });
+ });
+
+ suite('self removal when empty thread changed to editing:false', () => {
+ let threadEl: GrCommentThread;
+
+ setup(async () => {
+ threadEl = basicFixture.instantiate();
+ threadEl.thread = createThread();
+ });
+
+ test('new thread el normally has a parent and an unsaved comment', async () => {
+ await waitUntil(() => threadEl.editing);
+ assert.isOk(threadEl.unsavedComment);
+ assert.isOk(threadEl.parentElement);
+ });
+
+ test('thread el removed after clicking CANCEL', async () => {
+ await waitUntil(() => threadEl.editing);
+
+ const commentEl = queryAndAssert(threadEl, 'gr-comment');
+ const buttonEl = queryAndAssert(commentEl, 'gr-button.cancel');
+ tap(buttonEl);
+
+ await waitUntil(() => !threadEl.editing);
+ assert.isNotOk(threadEl.parentElement);
+ });
+ });
+
+ test('comments are sorted correctly', () => {
+ const comments: CommentInfo[] = [
{
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com' as EmailAddress,
- },
- id: 'baf0414d_60047215' as UrlEncodedCommentId,
- line: 5,
- message: 'is this a crossover episode!?',
- updated: '2015-12-08 19:48:33.843000000' as Timestamp,
- path: '/path/to/file.txt',
- unresolved: false,
+ id: 'jacks_confession' as UrlEncodedCommentId,
+ in_reply_to: 'sallys_confession' as UrlEncodedCommentId,
+ message: 'i like you, too',
+ updated: '2015-12-25 15:00:20.396000000' as Timestamp,
+ },
+ {
+ id: 'sallys_confession' as UrlEncodedCommentId,
+ message: 'i like you, jack',
+ updated: '2015-12-24 15:00:20.396000000' as Timestamp,
+ },
+ {
+ id: 'sally_to_dr_finklestein' as UrlEncodedCommentId,
+ message: 'i’m running away',
+ updated: '2015-10-31 09:00:20.396000000' as Timestamp,
+ },
+ {
+ id: 'sallys_defiance' as UrlEncodedCommentId,
+ in_reply_to: 'sally_to_dr_finklestein' as UrlEncodedCommentId,
+ message: 'i will poison you so i can get away',
+ updated: '2015-10-31 15:00:20.396000000' as Timestamp,
+ },
+ {
+ id: 'dr_finklesteins_response' as UrlEncodedCommentId,
+ in_reply_to: 'sally_to_dr_finklestein' as UrlEncodedCommentId,
+ message: 'no i will pull a thread and your arm will fall off',
+ updated: '2015-10-31 11:00:20.396000000' as Timestamp,
+ },
+ {
+ id: 'sallys_mission' as UrlEncodedCommentId,
+ message: 'i have to find santa',
+ updated: '2015-12-24 15:00:20.396000000' as Timestamp,
},
];
- flush();
- });
-
- test('ack and done should be hidden', () => {
- element.changeNum = 42 as NumericChangeId;
- element.patchNum = 1 as PatchSetNum;
-
- const commentEl = element.shadowRoot?.querySelector('gr-comment');
- assert.ok(commentEl);
-
- const ackBtn = element.shadowRoot?.querySelector('#ackBtn');
- const doneBtn = element.shadowRoot?.querySelector('#doneBtn');
- assert.equal(ackBtn, null);
- assert.equal(doneBtn, null);
- });
-
- test('reply and quote button should be visible', () => {
- const commentEl = element.shadowRoot?.querySelector('gr-comment');
- assert.ok(commentEl);
-
- const replyBtn = element.shadowRoot?.querySelector('#replyBtn');
- const quoteBtn = element.shadowRoot?.querySelector('#quoteBtn');
- assert.ok(replyBtn);
- assert.ok(quoteBtn);
+ const results = sortComments(comments);
+ assert.deepEqual(results, [
+ {
+ id: 'sally_to_dr_finklestein' as UrlEncodedCommentId,
+ message: 'i’m running away',
+ updated: '2015-10-31 09:00:20.396000000' as Timestamp,
+ },
+ {
+ id: 'dr_finklesteins_response' as UrlEncodedCommentId,
+ in_reply_to: 'sally_to_dr_finklestein' as UrlEncodedCommentId,
+ message: 'no i will pull a thread and your arm will fall off',
+ updated: '2015-10-31 11:00:20.396000000' as Timestamp,
+ },
+ {
+ id: 'sallys_defiance' as UrlEncodedCommentId,
+ in_reply_to: 'sally_to_dr_finklestein' as UrlEncodedCommentId,
+ message: 'i will poison you so i can get away',
+ updated: '2015-10-31 15:00:20.396000000' as Timestamp,
+ },
+ {
+ id: 'sallys_confession' as UrlEncodedCommentId,
+ message: 'i like you, jack',
+ updated: '2015-12-24 15:00:20.396000000' as Timestamp,
+ },
+ {
+ id: 'sallys_mission' as UrlEncodedCommentId,
+ message: 'i have to find santa',
+ updated: '2015-12-24 15:00:20.396000000' as Timestamp,
+ },
+ {
+ id: 'jacks_confession' as UrlEncodedCommentId,
+ in_reply_to: 'sallys_confession' as UrlEncodedCommentId,
+ message: 'i like you, too',
+ updated: '2015-12-25 15:00:20.396000000' as Timestamp,
+ },
+ ]);
});
});
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 154a045..c8dab62 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -27,52 +27,54 @@
import '../gr-tooltip-content/gr-tooltip-content';
import '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
import '../gr-account-label/gr-account-label';
-import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-comment_html';
-import {getRootElement} from '../../../scripts/rootElement';
-import {appContext} from '../../../services/app-context';
-import {customElement, observe, property} from '@polymer/decorators';
+import {getAppContext} from '../../../services/app-context';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
+import {resolve} from '../../../models/dependency';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {GrTextarea} from '../gr-textarea/gr-textarea';
import {GrOverlay} from '../gr-overlay/gr-overlay';
import {
AccountDetailInfo,
+ CommentLinks,
NumericChangeId,
- ConfigInfo,
- PatchSetNum,
RepoName,
- BasePatchSetNum,
+ RobotCommentInfo,
} from '../../../types/common';
-import {GrButton} from '../gr-button/gr-button';
import {GrConfirmDeleteCommentDialog} from '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
import {
- isDraft,
+ Comment,
+ DraftInfo,
+ isDraftOrUnsaved,
isRobot,
- UIComment,
- UIDraft,
- UIRobot,
+ isUnsaved,
} from '../../../utils/comment-util';
-import {OpenFixPreviewEventDetail} from '../../../types/events';
-import {fire, fireAlert, fireEvent} from '../../../utils/event-util';
-import {pluralize} from '../../../utils/string-util';
+import {
+ OpenFixPreviewEventDetail,
+ ValueChangedEvent,
+} from '../../../types/events';
+import {fire, fireEvent} from '../../../utils/event-util';
import {assertIsDefined} from '../../../utils/common-util';
-import {debounce, DelayedTask} from '../../../utils/async-util';
-import {StorageLocation} from '../../../services/storage/gr-storage';
-import {addShortcut, Key, Modifier} from '../../../utils/dom-util';
+import {Key, Modifier} from '../../../utils/dom-util';
+import {commentsModelToken} from '../../../models/comments/comments-model';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {subscribe} from '../../lit/subscription-controller';
+import {ShortcutController} from '../../lit/shortcut-controller';
+import {classMap} from 'lit/directives/class-map';
+import {LineNumber} from '../../../api/diff';
+import {CommentSide} from '../../../constants/constants';
+import {getRandomInt} from '../../../utils/math-util';
+import {Subject} from 'rxjs';
+import {debounceTime} from 'rxjs/operators';
+import {configModelToken} from '../../../models/config/config-model';
-const STORAGE_DEBOUNCE_INTERVAL = 400;
-const TOAST_DEBOUNCE_INTERVAL = 200;
-
-const SAVED_MESSAGE = 'All changes saved';
const UNSAVED_MESSAGE = 'Unable to save draft';
-const REPORT_CREATE_DRAFT = 'CreateDraftComment';
-const REPORT_UPDATE_DRAFT = 'UpdateDraftComment';
-const REPORT_DISCARD_DRAFT = 'DiscardDraftComment';
-
const FILE = 'FILE';
+// visible for testing
+export const AUTO_SAVE_DEBOUNCE_DELAY_MS = 2000;
+
export const __testOnly_UNSAVED_MESSAGE = UNSAVED_MESSAGE;
/**
@@ -87,25 +89,21 @@
'When disagreeing, explain the advantage of your approach.',
];
-interface CommentOverlays {
- confirmDelete?: GrOverlay | null;
- confirmDiscard?: GrOverlay | null;
+declare global {
+ interface HTMLElementEventMap {
+ 'comment-editing-changed': CustomEvent<boolean>;
+ 'comment-unresolved-changed': CustomEvent<boolean>;
+ 'comment-anchor-tap': CustomEvent<CommentAnchorTapEventDetail>;
+ }
}
-export interface GrComment {
- $: {
- container: HTMLDivElement;
- resolvedCheckbox: HTMLInputElement;
- header: HTMLDivElement;
- };
+export interface CommentAnchorTapEventDetail {
+ number: LineNumber;
+ side?: CommentSide;
}
@customElement('gr-comment')
-export class GrComment extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrComment extends LitElement {
/**
* Fired when the create fix comment action is triggered.
*
@@ -119,30 +117,6 @@
*/
/**
- * Fired when this comment is discarded.
- *
- * @event comment-discard
- */
-
- /**
- * Fired when this comment is edited.
- *
- * @event comment-edit
- */
-
- /**
- * Fired when this comment is saved.
- *
- * @event comment-save
- */
-
- /**
- * Fired when this comment is updated.
- *
- * @event comment-update
- */
-
- /**
* Fired when editing status changed.
*
* @event comment-editing-changed
@@ -154,192 +128,809 @@
* @event comment-anchor-tap
*/
- @property({type: Number})
- changeNum?: NumericChangeId;
+ @query('#editTextarea')
+ textarea?: GrTextarea;
- @property({type: String})
- projectName?: RepoName;
+ @query('#container')
+ container?: HTMLElement;
- @property({type: Object, notify: true, observer: '_commentChanged'})
- comment?: UIComment;
+ @query('#resolvedCheckbox')
+ resolvedCheckbox?: HTMLInputElement;
+ @query('#confirmDeleteOverlay')
+ confirmDeleteOverlay?: GrOverlay;
+
+ @property({type: Object})
+ comment?: Comment;
+
+ // TODO: Move this out of gr-comment. gr-comment should not have a comments
+ // property. This is only used for hasHumanReply at the moment.
@property({type: Array})
- comments?: UIComment[];
-
- @property({type: Boolean, reflectToAttribute: true})
- isRobotComment = false;
-
- @property({type: Boolean, reflectToAttribute: true})
- disabled = false;
-
- @property({type: Boolean, observer: '_draftChanged'})
- draft = false;
-
- @property({type: Boolean, observer: '_editingChanged'})
- editing = false;
-
- // Assigns a css property to the comment hiding the comment while it's being
- // discarded
- @property({
- type: Boolean,
- reflectToAttribute: true,
- })
- discarding = false;
-
- @property({type: Boolean})
- hasChildren?: boolean;
-
- @property({type: String})
- patchNum?: PatchSetNum;
-
- @property({type: Boolean})
- showActions?: boolean;
-
- @property({type: Boolean})
- _showHumanActions?: boolean;
-
- @property({type: Boolean})
- _showRobotActions?: boolean;
-
- @property({
- type: Boolean,
- reflectToAttribute: true,
- observer: '_toggleCollapseClass',
- })
- collapsed = true;
-
- @property({type: Object})
- projectConfig?: ConfigInfo;
-
- @property({type: Boolean})
- robotButtonDisabled = false;
-
- @property({type: Boolean})
- _hasHumanReply?: boolean;
-
- @property({type: Boolean})
- _isAdmin = false;
-
- @property({type: Object})
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- _xhrPromise?: Promise<any>; // Used for testing.
-
- @property({type: String, observer: '_messageTextChanged'})
- _messageText = '';
-
- @property({type: String})
- side?: string;
-
- @property({type: Boolean})
- resolved = false;
-
- // Intentional to share the object across instances.
- @property({type: Object})
- _numPendingDraftRequests: {number: number} = {number: 0};
-
- @property({type: Boolean})
- _enableOverlay = false;
+ comments?: Comment[];
/**
- * Property for storing references to overlay elements. When the overlays
- * are moved to getRootElement() to be shown they are no-longer
- * children, so they can't be queried along the tree, so they are stored
- * here.
+ * Initial collapsed state of the comment.
*/
- @property({type: Object})
- _overlays: CommentOverlays = {};
+ @property({type: Boolean, attribute: 'initially-collapsed'})
+ initiallyCollapsed?: boolean;
+
+ /**
+ * This is the *current* (internal) collapsed state of the comment. Do not set
+ * from the outside. Use `initiallyCollapsed` instead. This is just a
+ * reflected property such that css rules can be based on it.
+ */
+ @property({type: Boolean, reflect: true})
+ collapsed?: boolean;
+
+ @property({type: Boolean, attribute: 'robot-button-disabled'})
+ robotButtonDisabled = false;
+
+ /* internal only, but used in css rules */
+ @property({type: Boolean, reflect: true})
+ saving = false;
+
+ /**
+ * `saving` and `autoSaving` are separate and cannot be set at the same time.
+ * `saving` affects the UI state (disabled buttons, etc.) and eventually
+ * leaves editing mode, but `autoSaving` just happens in the background
+ * without the user noticing.
+ */
+ @state()
+ autoSaving?: Promise<DraftInfo>;
+
+ @state()
+ changeNum?: NumericChangeId;
+
+ @state()
+ editing = false;
+
+ @state()
+ commentLinks: CommentLinks = {};
+
+ @state()
+ repoName?: RepoName;
+
+ /* The 'dirty' state of the comment.message, which will be saved on demand. */
+ @state()
+ messageText = '';
+
+ /* The 'dirty' state of !comment.unresolved, which will be saved on demand. */
+ @state()
+ unresolved = true;
@property({type: Boolean})
- _showRespectfulTip = false;
+ showConfirmDeleteOverlay = false;
@property({type: Boolean})
- showPatchset = true;
+ showRespectfulTip = false;
@property({type: String})
- _respectfulReviewTip?: string;
+ respectfulReviewTip?: string;
@property({type: Boolean})
- _respectfulTipDismissed = false;
+ respectfulTipDismissed = false;
@property({type: Boolean})
- _unableToSave = false;
+ unableToSave = false;
- @property({type: Object})
- _selfAccount?: AccountDetailInfo;
+ @property({type: Boolean, attribute: 'show-patchset'})
+ showPatchset = false;
- @property({type: Boolean})
+ @property({type: Boolean, attribute: 'show-ported-comment'})
showPortedComment = false;
- /** Called in disconnectedCallback. */
- private cleanups: (() => void)[] = [];
+ @state()
+ account?: AccountDetailInfo;
- private readonly restApiService = appContext.restApiService;
+ @state()
+ isAdmin = false;
- private readonly storage = appContext.storageService;
+ private readonly restApiService = getAppContext().restApiService;
- private readonly reporting = appContext.reportingService;
+ private readonly storage = getAppContext().storageService;
- private readonly commentsService = appContext.commentsService;
+ private readonly reporting = getAppContext().reportingService;
- private fireUpdateTask?: DelayedTask;
+ private readonly changeModel = getAppContext().changeModel;
- private storeTask?: DelayedTask;
+ // Private but used in tests.
+ readonly getCommentsModel = resolve(this, commentsModelToken);
- private draftToastTask?: DelayedTask;
+ private readonly userModel = getAppContext().userModel;
- override connectedCallback() {
- super.connectedCallback();
- this.restApiService.getAccount().then(account => {
- this._selfAccount = account;
- });
- if (this.editing) {
- this.collapsed = false;
- } else if (this.comment) {
- this.collapsed = !!this.comment.collapsed;
- }
- this._getIsAdmin().then(isAdmin => {
- this._isAdmin = !!isAdmin;
- });
- this.cleanups.push(
- addShortcut(this, {key: Key.ESC}, e => this._handleEsc(e))
+ private readonly configModel = resolve(this, configModelToken);
+
+ private readonly shortcuts = new ShortcutController(this);
+
+ /**
+ * This is triggered when the user types into the editing textarea. We then
+ * debounce it and call autoSave().
+ */
+ private autoSaveTrigger$ = new Subject();
+
+ /**
+ * Set to the content of DraftInfo when entering editing mode.
+ * Only used for "Cancel".
+ */
+ private originalMessage = '';
+
+ /**
+ * Set to the content of DraftInfo when entering editing mode.
+ * Only used for "Cancel".
+ */
+ private originalUnresolved = false;
+
+ constructor() {
+ super();
+ subscribe(this, this.userModel.account$, x => (this.account = x));
+ subscribe(this, this.userModel.isAdmin$, x => (this.isAdmin = x));
+
+ subscribe(this, this.changeModel.repo$, x => (this.repoName = x));
+ subscribe(this, this.changeModel.changeNum$, x => (this.changeNum = x));
+ subscribe(
+ this,
+ this.autoSaveTrigger$.pipe(debounceTime(AUTO_SAVE_DEBOUNCE_DELAY_MS)),
+ () => {
+ this.autoSave();
+ }
);
+ this.shortcuts.addLocal({key: Key.ESC}, () => this.handleEsc());
for (const key of ['s', Key.ENTER]) {
for (const modifier of [Modifier.CTRL_KEY, Modifier.META_KEY]) {
- addShortcut(this, {key, modifiers: [modifier]}, e =>
- this._handleSaveKey(e)
- );
+ this.shortcuts.addLocal({key, modifiers: [modifier]}, () => {
+ this.save();
+ });
}
}
}
+ override connectedCallback() {
+ super.connectedCallback();
+ subscribe(
+ this,
+ this.configModel().repoCommentLinks$,
+ x => (this.commentLinks = x)
+ );
+ }
+
override disconnectedCallback() {
- for (const cleanup of this.cleanups) cleanup();
- this.cleanups = [];
- this.fireUpdateTask?.cancel();
- this.storeTask?.cancel();
- this.draftToastTask?.cancel();
- if (this.textarea) {
- this.textarea.closeDropdown();
- }
+ // Clean up emoji dropdown.
+ if (this.textarea) this.textarea.closeDropdown();
super.disconnectedCallback();
}
- /** 2nd argument is for *triggering* the computation only. */
- _getAuthor(comment?: UIComment, _?: unknown) {
- return comment?.author || this._selfAccount;
+ static override get styles() {
+ return [
+ sharedStyles,
+ css`
+ :host {
+ display: block;
+ font-family: var(--font-family);
+ padding: var(--spacing-m);
+ }
+ :host([collapsed]) {
+ padding: var(--spacing-s) var(--spacing-m);
+ }
+ :host([saving]) {
+ pointer-events: none;
+ }
+ :host([saving]) .actions,
+ :host([saving]) .robotActions,
+ :host([saving]) .date {
+ opacity: 0.5;
+ }
+ .body {
+ padding-top: var(--spacing-m);
+ }
+ .header {
+ align-items: center;
+ cursor: pointer;
+ display: flex;
+ }
+ .headerLeft > span {
+ font-weight: var(--font-weight-bold);
+ }
+ .headerMiddle {
+ color: var(--deemphasized-text-color);
+ flex: 1;
+ overflow: hidden;
+ }
+ .draftLabel,
+ .draftTooltip {
+ color: var(--deemphasized-text-color);
+ display: inline;
+ }
+ .date {
+ justify-content: flex-end;
+ text-align: right;
+ white-space: nowrap;
+ }
+ span.date {
+ color: var(--deemphasized-text-color);
+ }
+ span.date:hover {
+ text-decoration: underline;
+ }
+ .actions,
+ .robotActions {
+ display: flex;
+ justify-content: flex-end;
+ padding-top: 0;
+ }
+ .robotActions {
+ /* Better than the negative margin would be to remove the gr-button
+ * padding, but then we would also need to fix the buttons that are
+ * inserted by plugins. :-/ */
+ margin: 4px 0 -4px;
+ }
+ .action {
+ margin-left: var(--spacing-l);
+ }
+ .rightActions {
+ display: flex;
+ justify-content: flex-end;
+ }
+ .rightActions gr-button {
+ --gr-button-padding: 0 var(--spacing-s);
+ }
+ .editMessage {
+ display: block;
+ margin: var(--spacing-m) 0;
+ width: 100%;
+ }
+ .show-hide {
+ margin-left: var(--spacing-s);
+ }
+ .robotId {
+ color: var(--deemphasized-text-color);
+ margin-bottom: var(--spacing-m);
+ }
+ .robotRun {
+ margin-left: var(--spacing-m);
+ }
+ .robotRunLink {
+ margin-left: var(--spacing-m);
+ }
+ /* just for a11y */
+ input.show-hide {
+ display: none;
+ }
+ label.show-hide {
+ cursor: pointer;
+ display: block;
+ }
+ label.show-hide iron-icon {
+ vertical-align: top;
+ }
+ :host([collapsed]) #container .body {
+ padding-top: 0;
+ }
+ #container .collapsedContent {
+ display: block;
+ overflow: hidden;
+ padding-left: var(--spacing-m);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .resolve,
+ .unresolved {
+ align-items: center;
+ display: flex;
+ flex: 1;
+ margin: 0;
+ }
+ .resolve label {
+ color: var(--comment-text-color);
+ }
+ gr-dialog .main {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ }
+ #deleteBtn {
+ --gr-button-text-color: var(--deemphasized-text-color);
+ --gr-button-padding: 0;
+ }
+
+ /** Disable select for the caret and actions */
+ .actions,
+ .show-hide {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ }
+
+ .respectfulReviewTip {
+ justify-content: space-between;
+ display: flex;
+ padding: var(--spacing-m);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius);
+ margin-bottom: var(--spacing-m);
+ }
+ .respectfulReviewTip div {
+ display: flex;
+ }
+ .respectfulReviewTip div iron-icon {
+ margin-right: var(--spacing-s);
+ }
+ .respectfulReviewTip a {
+ white-space: nowrap;
+ margin-right: var(--spacing-s);
+ padding-left: var(--spacing-m);
+ text-decoration: none;
+ }
+ .pointer {
+ cursor: pointer;
+ }
+ .patchset-text {
+ color: var(--deemphasized-text-color);
+ margin-left: var(--spacing-s);
+ }
+ .headerLeft gr-account-label {
+ --account-max-length: 130px;
+ width: 150px;
+ }
+ .headerLeft gr-account-label::part(gr-account-label-text) {
+ font-weight: var(--font-weight-bold);
+ }
+ .draft gr-account-label {
+ width: unset;
+ }
+ .portedMessage {
+ margin: 0 var(--spacing-m);
+ }
+ .link-icon {
+ cursor: pointer;
+ }
+ `,
+ ];
}
- _getUrlForComment(comment?: UIComment) {
- if (!comment || !this.changeNum || !this.projectName) return '';
+ override render() {
+ if (isUnsaved(this.comment) && !this.editing) return;
+ const classes = {container: true, draft: isDraftOrUnsaved(this.comment)};
+ return html`
+ <div id="container" class="${classMap(classes)}">
+ <div
+ class="header"
+ id="header"
+ @click="${() => (this.collapsed = !this.collapsed)}"
+ >
+ <div class="headerLeft">
+ ${this.renderAuthor()} ${this.renderPortedCommentMessage()}
+ ${this.renderDraftLabel()}
+ </div>
+ <div class="headerMiddle">${this.renderCollapsedContent()}</div>
+ ${this.renderRunDetails()} ${this.renderDeleteButton()}
+ ${this.renderPatchset()} ${this.renderDate()} ${this.renderToggle()}
+ </div>
+ <div class="body">
+ ${this.renderRobotAuthor()} ${this.renderEditingTextarea()}
+ ${this.renderRespectfulTip()} ${this.renderCommentMessage()}
+ ${this.renderHumanActions()} ${this.renderRobotActions()}
+ </div>
+ </div>
+ ${this.renderConfirmDialog()}
+ `;
+ }
+
+ private renderAuthor() {
+ if (isRobot(this.comment)) {
+ const id = this.comment.robot_id;
+ return html`<span class="robotName">${id}</span>`;
+ }
+ const classes = {draft: isDraftOrUnsaved(this.comment)};
+ return html`
+ <gr-account-label
+ .account="${this.comment?.author ?? this.account}"
+ class="${classMap(classes)}"
+ hideStatus
+ >
+ </gr-account-label>
+ `;
+ }
+
+ private renderPortedCommentMessage() {
+ if (!this.showPortedComment) return;
+ if (!this.comment?.patch_set) return;
+ return html`
+ <a href="${this.getUrlForComment()}">
+ <span class="portedMessage" @click="${this.handlePortedMessageClick}">
+ From patchset ${this.comment?.patch_set}
+ </span>
+ </a>
+ `;
+ }
+
+ private renderDraftLabel() {
+ if (!isDraftOrUnsaved(this.comment)) return;
+ let label = 'DRAFT';
+ let tooltip =
+ 'This draft is only visible to you. ' +
+ "To publish drafts, click the 'Reply' or 'Start review' button " +
+ "at the top of the change or press the 'a' key.";
+ if (this.unableToSave) {
+ label += ' (Failed to save)';
+ tooltip = 'Unable to save draft. Please try to save again.';
+ }
+ return html`
+ <gr-tooltip-content
+ class="draftTooltip"
+ has-tooltip
+ title="${tooltip}"
+ max-width="20em"
+ show-icon
+ >
+ <span class="draftLabel">${label}</span>
+ </gr-tooltip-content>
+ `;
+ }
+
+ private renderCollapsedContent() {
+ if (!this.collapsed) return;
+ return html`
+ <span class="collapsedContent">${this.comment?.message}</span>
+ `;
+ }
+
+ private renderRunDetails() {
+ if (!isRobot(this.comment)) return;
+ if (!this.comment?.url || this.collapsed) return;
+ return html`
+ <div class="runIdMessage message">
+ <div class="runIdInformation">
+ <a class="robotRunLink" href="${this.comment.url}">
+ <span class="robotRun link">Run Details</span>
+ </a>
+ </div>
+ </div>
+ `;
+ }
+
+ /**
+ * Deleting a comment is an admin feature. It means more than just discarding
+ * a draft. It is an action applied to published comments.
+ */
+ private renderDeleteButton() {
+ if (
+ !this.isAdmin ||
+ isDraftOrUnsaved(this.comment) ||
+ isRobot(this.comment)
+ )
+ return;
+ if (this.collapsed) return;
+ return html`
+ <gr-button
+ id="deleteBtn"
+ title="Delete Comment"
+ link
+ class="action delete"
+ @click="${this.openDeleteCommentOverlay}"
+ >
+ <iron-icon id="icon" icon="gr-icons:delete"></iron-icon>
+ </gr-button>
+ `;
+ }
+
+ private renderPatchset() {
+ if (!this.showPatchset) return;
+ assertIsDefined(this.comment?.patch_set, 'comment.patch_set');
+ return html`
+ <span class="patchset-text"> Patchset ${this.comment.patch_set}</span>
+ `;
+ }
+
+ private renderDate() {
+ if (!this.comment?.updated || this.collapsed) return;
+ return html`
+ <span class="separator"></span>
+ <span class="date" tabindex="0" @click="${this.handleAnchorClick}">
+ <gr-date-formatter
+ withTooltip
+ .dateStr="${this.comment.updated}"
+ ></gr-date-formatter>
+ </span>
+ `;
+ }
+
+ private renderToggle() {
+ const icon = this.collapsed
+ ? 'gr-icons:expand-more'
+ : 'gr-icons:expand-less';
+ const ariaLabel = this.collapsed ? 'Expand' : 'Collapse';
+ return html`
+ <div class="show-hide" tabindex="0">
+ <label class="show-hide" aria-label="${ariaLabel}">
+ <input
+ type="checkbox"
+ class="show-hide"
+ ?checked="${this.collapsed}"
+ @change="${() => (this.collapsed = !this.collapsed)}"
+ />
+ <iron-icon id="icon" icon="${icon}"></iron-icon>
+ </label>
+ </div>
+ `;
+ }
+
+ private renderRobotAuthor() {
+ if (!isRobot(this.comment) || this.collapsed) return;
+ return html`<div class="robotId">${this.comment.author?.name}</div>`;
+ }
+
+ private renderEditingTextarea() {
+ if (!this.editing || this.collapsed) return;
+ return html`
+ <gr-textarea
+ id="editTextarea"
+ class="editMessage"
+ autocomplete="on"
+ code=""
+ ?disabled="${this.saving}"
+ rows="4"
+ text="${this.messageText}"
+ @text-changed="${(e: ValueChangedEvent) => {
+ // TODO: This is causing a re-render of <gr-comment> on every key
+ // press. Try to avoid always setting `this.messageText` or at least
+ // debounce it. Most of the code can just inspect the current value
+ // of the textare instead of needing a dedicated property.
+ this.messageText = e.detail.value;
+ this.autoSaveTrigger$.next();
+ }}"
+ ></gr-textarea>
+ `;
+ }
+
+ private renderRespectfulTip() {
+ if (!this.showRespectfulTip || this.respectfulTipDismissed) return;
+ if (!this.editing || this.collapsed) return;
+ return html`
+ <div class="respectfulReviewTip">
+ <div>
+ <gr-tooltip-content
+ has-tooltip
+ title="Tips for respectful code reviews."
+ >
+ <iron-icon
+ class="pointer"
+ icon="gr-icons:lightbulb-outline"
+ ></iron-icon>
+ </gr-tooltip-content>
+ ${this.respectfulReviewTip}
+ </div>
+ <div>
+ <a
+ tabindex="-1"
+ @click="${this.onRespectfulReadMoreClick}"
+ href="https://testing.googleblog.com/2019/11/code-health-respectful-reviews-useful.html"
+ target="_blank"
+ >
+ Read more
+ </a>
+ <a
+ tabindex="-1"
+ class="close pointer"
+ @click="${this.dismissRespectfulTip}"
+ >
+ Not helpful
+ </a>
+ </div>
+ </div>
+ `;
+ }
+
+ private renderCommentMessage() {
+ if (this.collapsed || this.editing) return;
+ return html`
+ <!--The "message" class is needed to ensure selectability from
+ gr-diff-selection.-->
+ <gr-formatted-text
+ class="message"
+ .content="${this.comment?.message}"
+ .config="${this.commentLinks}"
+ ?noTrailingMargin="${!isDraftOrUnsaved(this.comment)}"
+ ></gr-formatted-text>
+ `;
+ }
+
+ private renderCopyLinkIcon() {
+ // Only show the icon when the thread contains a published comment.
+ if (!this.comment?.in_reply_to && isDraftOrUnsaved(this.comment)) return;
+ return html`
+ <iron-icon
+ class="copy link-icon"
+ @click="${this.handleCopyLink}"
+ title="Copy link to this comment"
+ icon="gr-icons:link"
+ role="button"
+ tabindex="0"
+ >
+ </iron-icon>
+ `;
+ }
+
+ private renderHumanActions() {
+ if (!this.account || isRobot(this.comment)) return;
+ if (this.collapsed || !isDraftOrUnsaved(this.comment)) return;
+ return html`
+ <div class="actions">
+ <div class="action resolve">
+ <label>
+ <input
+ type="checkbox"
+ id="resolvedCheckbox"
+ ?checked="${!this.unresolved}"
+ @change="${this.handleToggleResolved}"
+ />
+ Resolved
+ </label>
+ </div>
+ ${this.renderDraftActions()}
+ </div>
+ `;
+ }
+
+ private renderDraftActions() {
+ if (!isDraftOrUnsaved(this.comment)) return;
+ return html`
+ <div class="rightActions">
+ ${this.autoSaving ? html`. ` : ''}
+ ${this.renderCopyLinkIcon()} ${this.renderDiscardButton()}
+ ${this.renderEditButton()} ${this.renderCancelButton()}
+ ${this.renderSaveButton()}
+ </div>
+ `;
+ }
+
+ private renderDiscardButton() {
+ if (this.editing) return;
+ return html`<gr-button
+ link
+ ?disabled="${this.saving}"
+ class="action discard"
+ @click="${this.discard}"
+ >Discard</gr-button
+ >`;
+ }
+
+ private renderEditButton() {
+ if (this.editing) return;
+ return html`<gr-button
+ link
+ ?disabled="${this.saving}"
+ class="action edit"
+ @click="${this.edit}"
+ >Edit</gr-button
+ >`;
+ }
+
+ private renderCancelButton() {
+ if (!this.editing) return;
+ return html`
+ <gr-button
+ link
+ ?disabled="${this.saving}"
+ class="action cancel"
+ @click="${this.cancel}"
+ >Cancel</gr-button
+ >
+ `;
+ }
+
+ private renderSaveButton() {
+ if (!this.editing && !this.unableToSave) return;
+ return html`
+ <gr-button
+ link
+ ?disabled="${this.isSaveDisabled()}"
+ class="action save"
+ @click="${this.save}"
+ >Save</gr-button
+ >
+ `;
+ }
+
+ private renderRobotActions() {
+ if (!this.account || !isRobot(this.comment)) return;
+ const endpoint = html`
+ <gr-endpoint-decorator name="robot-comment-controls">
+ <gr-endpoint-param name="comment" .value="${this.comment}">
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ `;
+ return html`
+ <div class="robotActions">
+ ${this.renderCopyLinkIcon()} ${endpoint} ${this.renderShowFixButton()}
+ ${this.renderPleaseFixButton()}
+ </div>
+ `;
+ }
+
+ private renderShowFixButton() {
+ if (!(this.comment as RobotCommentInfo)?.fix_suggestions) return;
+ return html`
+ <gr-button
+ link
+ secondary
+ class="action show-fix"
+ ?disabled="${this.saving}"
+ @click="${this.handleShowFix}"
+ >
+ Show Fix
+ </gr-button>
+ `;
+ }
+
+ private renderPleaseFixButton() {
+ if (this.hasHumanReply()) return;
+ return html`
+ <gr-button
+ link
+ ?disabled="${this.robotButtonDisabled}"
+ class="action fix"
+ @click="${this.handleFix}"
+ >
+ Please Fix
+ </gr-button>
+ `;
+ }
+
+ private renderConfirmDialog() {
+ if (!this.showConfirmDeleteOverlay) return;
+ return html`
+ <gr-overlay id="confirmDeleteOverlay" with-backdrop>
+ <gr-confirm-delete-comment-dialog
+ id="confirmDeleteComment"
+ @confirm="${this.handleConfirmDeleteComment}"
+ @cancel="${this.closeDeleteCommentOverlay}"
+ >
+ </gr-confirm-delete-comment-dialog>
+ </gr-overlay>
+ `;
+ }
+
+ private getUrlForComment() {
+ const comment = this.comment;
+ if (!comment || !this.changeNum || !this.repoName) return '';
if (!comment.id) throw new Error('comment must have an id');
return GerritNav.getUrlForComment(
this.changeNum as NumericChangeId,
- this.projectName,
+ this.repoName,
comment.id
);
}
- _handlePortedMessageClick() {
+ private firstWillUpdateDone = false;
+
+ firstWillUpdate() {
+ if (this.firstWillUpdateDone) return;
+ this.firstWillUpdateDone = true;
+
+ assertIsDefined(this.comment, 'comment');
+ this.unresolved = this.comment.unresolved ?? true;
+ if (isUnsaved(this.comment)) this.editing = true;
+ if (isDraftOrUnsaved(this.comment)) {
+ this.collapsed = false;
+ } else {
+ this.collapsed = !!this.initiallyCollapsed;
+ }
+ }
+
+ override willUpdate(changed: PropertyValues) {
+ this.firstWillUpdate();
+ if (changed.has('editing')) {
+ this.onEditingChanged();
+ }
+ if (changed.has('unresolved')) {
+ // The <gr-comment-thread> component wants to change its color based on
+ // the (dirty) unresolved state, so let's notify it about changes.
+ fire(this, 'comment-unresolved-changed', this.unresolved);
+ }
+ }
+
+ private handlePortedMessageClick() {
assertIsDefined(this.comment, 'comment');
this.reporting.reportInteraction('navigate-to-original-comment', {
line: this.comment.line,
@@ -347,740 +938,257 @@
});
}
- @observe('editing')
- _onEditingChange(editing?: boolean) {
- this.dispatchEvent(
- new CustomEvent('comment-editing-changed', {
- detail: !!editing,
- bubbles: true,
- composed: true,
- })
- );
- if (!editing) return;
- // visibility based on cache this will make sure we only and always show
- // a tip once every Math.max(a day, period between creating comments)
- const cachedVisibilityOfRespectfulTip =
- this.storage.getRespectfulTipVisibility();
- if (!cachedVisibilityOfRespectfulTip) {
- // we still want to show the tip with a probability of 30%
- if (this.getRandomNum(0, 3) >= 1) return;
- this._showRespectfulTip = true;
- const randomIdx = this.getRandomNum(0, RESPECTFUL_REVIEW_TIPS.length);
- this._respectfulReviewTip = RESPECTFUL_REVIEW_TIPS[randomIdx];
- this.reporting.reportInteraction('respectful-tip-appeared', {
- tip: this._respectfulReviewTip,
- });
- // update cache
- this.storage.setRespectfulTipVisibility();
- }
+ // private, but visible for testing
+ getRandomInt(from: number, to: number) {
+ return getRandomInt(from, to);
}
- /** Set as a separate method so easy to stub. */
- getRandomNum(min: number, max: number) {
- return Math.floor(Math.random() * (max - min) + min);
- }
-
- _computeVisibilityOfTip(showTip: boolean, tipDismissed: boolean) {
- return showTip && !tipDismissed;
- }
-
- _dismissRespectfulTip() {
- this._respectfulTipDismissed = true;
+ private dismissRespectfulTip() {
+ this.respectfulTipDismissed = true;
this.reporting.reportInteraction('respectful-tip-dismissed', {
- tip: this._respectfulReviewTip,
+ tip: this.respectfulReviewTip,
});
// add a 14-day delay to the tip cache
this.storage.setRespectfulTipVisibility(/* delayDays= */ 14);
}
- _onRespectfulReadMoreClick() {
+ private onRespectfulReadMoreClick() {
this.reporting.reportInteraction('respectful-read-more-clicked');
}
- get textarea(): GrTextarea | null {
- return this.shadowRoot?.querySelector('#editTextarea') as GrTextarea | null;
- }
-
- get confirmDeleteOverlay() {
- if (!this._overlays.confirmDelete) {
- this._enableOverlay = true;
- flush();
- this._overlays.confirmDelete = this.shadowRoot?.querySelector(
- '#confirmDeleteOverlay'
- ) as GrOverlay | null;
- }
- return this._overlays.confirmDelete;
- }
-
- get confirmDiscardOverlay() {
- if (!this._overlays.confirmDiscard) {
- this._enableOverlay = true;
- flush();
- this._overlays.confirmDiscard = this.shadowRoot?.querySelector(
- '#confirmDiscardOverlay'
- ) as GrOverlay | null;
- }
- return this._overlays.confirmDiscard;
- }
-
- _computeShowHideIcon(collapsed: boolean) {
- return collapsed ? 'gr-icons:expand-more' : 'gr-icons:expand-less';
- }
-
- _computeShowHideAriaLabel(collapsed: boolean) {
- return collapsed ? 'Expand' : 'Collapse';
- }
-
- @observe('showActions', 'isRobotComment')
- _calculateActionstoShow(showActions?: boolean, isRobotComment?: boolean) {
- // Polymer 2: check for undefined
- if ([showActions, isRobotComment].includes(undefined)) {
- return;
- }
-
- this._showHumanActions = showActions && !isRobotComment;
- this._showRobotActions = showActions && isRobotComment;
- }
-
- hasPublishedComment(comments?: UIComment[]) {
- if (!comments?.length) return false;
- return comments.length > 1 || !isDraft(comments[0]);
- }
-
- @observe('comment')
- _isRobotComment(comment: UIRobot) {
- this.isRobotComment = !!comment.robot_id;
- }
-
- isOnParent() {
- return this.side === 'PARENT';
- }
-
- _getIsAdmin() {
- return this.restApiService.getIsAdmin();
- }
-
- _computeDraftTooltip(unableToSave: boolean) {
- return unableToSave
- ? 'Unable to save draft. Please try to save again.'
- : "This draft is only visible to you. To publish drafts, click the 'Reply'" +
- "or 'Start review' button at the top of the change or press the 'A' key.";
- }
-
- _computeDraftText(unableToSave: boolean) {
- return 'DRAFT' + (unableToSave ? '(Failed to save)' : '');
- }
-
- handleCopyLink() {
+ private handleCopyLink() {
fireEvent(this, 'copy-comment-link');
}
- save(opt_comment?: UIComment) {
- let comment = opt_comment;
- if (!comment) {
- comment = this.comment;
+ /** Enter editing mode. */
+ private edit() {
+ if (!isDraftOrUnsaved(this.comment)) {
+ throw new Error('Cannot edit published comment.');
}
-
- this.set('comment.message', this._messageText);
- this.editing = false;
- this.disabled = true;
-
- if (!this._messageText) {
- return this._discardDraft();
- }
-
- this._xhrPromise = this._saveDraft(comment)
- .then(response => {
- this.disabled = false;
- if (!response.ok) {
- return;
- }
-
- this._eraseDraftCommentFromStorage();
- return this.restApiService.getResponseObject(response).then(obj => {
- const resComment = obj as unknown as UIDraft;
- if (!isDraft(this.comment)) throw new Error('Can only save drafts.');
- resComment.__draft = true;
- // Maintain the ephemeral draft ID for identification by other
- // elements.
- if (this.comment?.__draftID) {
- resComment.__draftID = this.comment.__draftID;
- }
- if (!resComment.patch_set) resComment.patch_set = this.patchNum;
- this.comment = resComment;
- this._fireSave();
- return obj;
- });
- })
- .catch(err => {
- this.disabled = false;
- throw err;
- });
-
- return this._xhrPromise;
- }
-
- _eraseDraftCommentFromStorage() {
- // Prevents a race condition in which removing the draft comment occurs
- // prior to it being saved.
- this.storeTask?.cancel();
-
- assertIsDefined(this.comment?.path, 'comment.path');
- assertIsDefined(this.changeNum, 'changeNum');
- this.storage.eraseDraftComment({
- changeNum: this.changeNum,
- patchNum: this._getPatchNum(),
- path: this.comment.path,
- line: this.comment.line,
- range: this.comment.range,
- });
- }
-
- _commentChanged(comment: UIComment) {
- this.editing = isDraft(comment) && !!comment.__editing;
- this.resolved = !comment.unresolved;
- this.discarding = false;
- if (this.editing) {
- // It's a new draft/reply, notify.
- this._fireUpdate();
- }
- }
-
- @observe('comment', 'comments.*')
- _computeHasHumanReply() {
- const comment = this.comment;
- if (!comment || !this.comments) return;
- // hide please fix button for robot comment that has human reply
- this._hasHumanReply = this.comments.some(
- c =>
- c.in_reply_to &&
- c.in_reply_to === comment.id &&
- !(c as UIRobot).robot_id
- );
- }
-
- _getEventPayload(): OpenFixPreviewEventDetail {
- return {comment: this.comment, patchNum: this.patchNum};
- }
-
- _fireEdit() {
- if (this.comment) this.commentsService.editDraft(this.comment);
- this.dispatchEvent(
- new CustomEvent('comment-edit', {
- detail: this._getEventPayload(),
- composed: true,
- bubbles: true,
- })
- );
- }
-
- _fireSave() {
- if (this.comment) this.commentsService.addDraft(this.comment);
- this.dispatchEvent(
- new CustomEvent('comment-save', {
- detail: this._getEventPayload(),
- composed: true,
- bubbles: true,
- })
- );
- }
-
- _fireUpdate() {
- this.fireUpdateTask = debounce(this.fireUpdateTask, () => {
- this.dispatchEvent(
- new CustomEvent('comment-update', {
- detail: this._getEventPayload(),
- composed: true,
- bubbles: true,
- })
- );
- });
- }
-
- _computeAccountLabelClass(draft: boolean) {
- return draft ? 'draft' : '';
- }
-
- _draftChanged(draft: boolean) {
- this.$.container.classList.toggle('draft', draft);
- }
-
- _editingChanged(editing?: boolean, previousValue?: boolean) {
- // Polymer 2: observer fires when at least one property is defined.
- // Do nothing to prevent comment.__editing being overwritten
- // if previousValue is undefined
- if (previousValue === undefined) return;
-
- this.$.container.classList.toggle('editing', editing);
- if (this.comment && this.comment.id) {
- const cancelButton = this.shadowRoot?.querySelector(
- '.cancel'
- ) as GrButton | null;
- if (cancelButton) {
- cancelButton.hidden = !editing;
- }
- }
- if (isDraft(this.comment)) {
- this.comment.__editing = this.editing;
- }
- if (!!editing !== !!previousValue) {
- // To prevent event firing on comment creation.
- this._fireUpdate();
- }
- if (editing) {
- setTimeout(() => {
- flush();
- this.textarea && this.textarea.putCursorAtEnd();
- }, 1);
- }
- }
-
- _computeDeleteButtonClass(isAdmin: boolean, draft: boolean) {
- return isAdmin && !draft ? 'showDeleteButtons' : '';
- }
-
- _computeSaveDisabled(
- draft: string,
- comment: UIComment | undefined,
- resolved?: boolean
- ) {
- // If resolved state has changed and a msg exists, save should be enabled.
- if (!comment || (comment.unresolved === resolved && draft)) {
- return false;
- }
- return !draft || draft.trim() === '';
- }
-
- _handleSaveKey(e: Event) {
- if (
- !this._computeSaveDisabled(this._messageText, this.comment, this.resolved)
- ) {
- e.preventDefault();
- this._handleSave(e);
- }
- }
-
- _handleEsc(e: Event) {
- if (!this._messageText.length) {
- e.preventDefault();
- this._handleCancel(e);
- }
- }
-
- _handleToggleCollapsed() {
- this.collapsed = !this.collapsed;
- }
-
- _toggleCollapseClass(collapsed: boolean) {
- if (collapsed) {
- this.$.container.classList.add('collapsed');
- } else {
- this.$.container.classList.remove('collapsed');
- }
- }
-
- @observe('comment.message')
- _commentMessageChanged(message: string) {
- /*
- * Only overwrite the message text user has typed if there is no existing
- * text typed by the user. This prevents the bug where creating another
- * comment triggered a recomputation of comments and the text written by
- * the user was lost.
- */
- if (!this._messageText || !this.editing) this._messageText = message || '';
- }
-
- _messageTextChanged(_: string, oldValue: string) {
- // Only store comments that are being edited in local storage.
- if (
- !this.comment ||
- (this.comment.id && (!isDraft(this.comment) || !this.comment.__editing))
- ) {
- return;
- }
-
- const patchNum = this.comment.patch_set
- ? this.comment.patch_set
- : this._getPatchNum();
- const {path, line, range} = this.comment;
- if (!path) return;
- this.storeTask = debounce(
- this.storeTask,
- () => {
- const message = this._messageText;
- if (this.changeNum === undefined) {
- throw new Error('undefined changeNum');
- }
- const commentLocation: StorageLocation = {
- changeNum: this.changeNum,
- patchNum,
- path,
- line,
- range,
- };
-
- if ((!message || !message.length) && oldValue) {
- // If the draft has been modified to be empty, then erase the storage
- // entry.
- this.storage.eraseDraftComment(commentLocation);
- } else {
- this.storage.setDraftComment(commentLocation, message);
- }
- },
- STORAGE_DEBOUNCE_INTERVAL
- );
- }
-
- _handleAnchorClick(e: Event) {
- e.preventDefault();
- if (!this.comment) return;
- this.dispatchEvent(
- new CustomEvent('comment-anchor-tap', {
- bubbles: true,
- composed: true,
- detail: {
- number: this.comment.line || FILE,
- side: this.side,
- },
- })
- );
- }
-
- _handleEdit(e: Event) {
- e.preventDefault();
- if (this.comment?.message) this._messageText = this.comment.message;
+ if (this.editing) return;
this.editing = true;
- this._fireEdit();
- this.reporting.recordDraftInteraction();
}
- _handleSave(e: Event) {
- e.preventDefault();
-
- // Ignore saves started while already saving.
- if (this.disabled) return;
- const timingLabel = this.comment?.id
- ? REPORT_UPDATE_DRAFT
- : REPORT_CREATE_DRAFT;
- const timer = this.reporting.getTimer(timingLabel);
- this.set('comment.__editing', false);
- return this.save().then(() => {
- timer.end();
- });
+ // TODO: Move this out of gr-comment. gr-comment should not have a comments
+ // property.
+ private hasHumanReply() {
+ if (!this.comment || !this.comments) return false;
+ return this.comments.some(
+ c => c.in_reply_to && c.in_reply_to === this.comment?.id && !isRobot(c)
+ );
}
- _handleCancel(e: Event) {
- e.preventDefault();
- if (!this.comment) return;
- if (!this.comment.id) {
- // Ensures we update the discarded draft message before deleting the draft
- this.set('comment.message', this._messageText);
- this._fireDiscard();
+ // private, but visible for testing
+ getEventPayload(): OpenFixPreviewEventDetail {
+ assertIsDefined(this.comment?.patch_set, 'comment.patch_set');
+ return {comment: this.comment, patchNum: this.comment.patch_set};
+ }
+
+ private onEditingChanged() {
+ if (this.editing) {
+ this.collapsed = false;
+ this.messageText = this.comment?.message ?? '';
+ this.unresolved = this.comment?.unresolved ?? true;
+ this.originalMessage = this.messageText;
+ this.originalUnresolved = this.unresolved;
+ setTimeout(() => this.textarea?.putCursorAtEnd(), 1);
+ }
+ this.setRespectfulTip();
+
+ // Parent components such as the reply dialog might be interested in whether
+ // come of their child components are in editing mode.
+ fire(this, 'comment-editing-changed', this.editing);
+ }
+
+ private setRespectfulTip() {
+ // visibility based on cache this will make sure we only and always show
+ // a tip once every Math.max(a day, period between creating comments)
+ const cachedVisibilityOfRespectfulTip =
+ this.storage.getRespectfulTipVisibility();
+ if (this.editing && !cachedVisibilityOfRespectfulTip) {
+ // we still want to show the tip with a probability of 33%
+ if (this.getRandomInt(0, 2) >= 1) return;
+ this.showRespectfulTip = true;
+ const randomIdx = this.getRandomInt(0, RESPECTFUL_REVIEW_TIPS.length);
+ this.respectfulReviewTip = RESPECTFUL_REVIEW_TIPS[randomIdx];
+ this.reporting.reportInteraction('respectful-tip-appeared', {
+ tip: this.respectfulReviewTip,
+ });
+ // update cache
+ this.storage.setRespectfulTipVisibility();
} else {
- this.set('comment.__editing', false);
- this.commentsService.cancelDraft(this.comment);
- this.editing = false;
+ this.showRespectfulTip = false;
+ this.respectfulReviewTip = undefined;
}
}
- _fireDiscard() {
- if (this.comment) this.commentsService.deleteDraft(this.comment);
- this.fireUpdateTask?.cancel();
- this.dispatchEvent(
- new CustomEvent('comment-discard', {
- detail: this._getEventPayload(),
- composed: true,
- bubbles: true,
- })
- );
+ // private, but visible for testing
+ isSaveDisabled() {
+ assertIsDefined(this.comment, 'comment');
+ if (this.saving) return true;
+ if (this.comment.unresolved !== this.unresolved) return false;
+ return !this.messageText?.trimEnd();
}
- _handleFix() {
- this.dispatchEvent(
- new CustomEvent('create-fix-comment', {
- bubbles: true,
- composed: true,
- detail: this._getEventPayload(),
- })
- );
+ private handleEsc() {
+ // vim users don't like ESC to cancel/discard, so only do this when the
+ // comment text is empty.
+ if (!this.messageText?.trimEnd()) this.cancel();
}
- _handleShowFix() {
- this.dispatchEvent(
- new CustomEvent('open-fix-preview', {
- bubbles: true,
- composed: true,
- detail: this._getEventPayload(),
- })
- );
- }
-
- _hasNoFix(comment?: UIComment) {
- return !comment || !(comment as UIRobot).fix_suggestions;
- }
-
- _handleDiscard(e: Event) {
- e.preventDefault();
- this.reporting.recordDraftInteraction();
-
- this._discardDraft();
- }
-
- _discardDraft() {
- if (!this.comment) return Promise.reject(new Error('undefined comment'));
- if (!isDraft(this.comment)) {
- return Promise.reject(new Error('Cannot discard a non-draft comment.'));
- }
- this.discarding = true;
- const timer = this.reporting.getTimer(REPORT_DISCARD_DRAFT);
- this.editing = false;
- this.disabled = true;
- this._eraseDraftCommentFromStorage();
-
- if (!this.comment.id) {
- this.disabled = false;
- this._fireDiscard();
- return Promise.resolve();
- }
-
- this._xhrPromise = this._deleteDraft(this.comment)
- .then(response => {
- this.disabled = false;
- if (!response.ok) {
- this.discarding = false;
- }
- timer.end();
- this._fireDiscard();
- return response;
- })
- .catch(err => {
- this.disabled = false;
- throw err;
- });
-
- return this._xhrPromise;
- }
-
- _getSavingMessage(numPending: number, requestFailed?: boolean) {
- if (requestFailed) {
- return UNSAVED_MESSAGE;
- }
- if (numPending === 0) {
- return SAVED_MESSAGE;
- }
- return `Saving ${pluralize(numPending, 'draft')}...`;
- }
-
- _showStartRequest() {
- const numPending = ++this._numPendingDraftRequests.number;
- this._updateRequestToast(numPending);
- }
-
- _showEndRequest() {
- const numPending = --this._numPendingDraftRequests.number;
- this._updateRequestToast(numPending);
- }
-
- _handleFailedDraftRequest() {
- this._numPendingDraftRequests.number--;
-
- // Cancel the debouncer so that error toasts from the error-manager will
- // not be overridden.
- this.draftToastTask?.cancel();
- this._updateRequestToast(
- this._numPendingDraftRequests.number,
- /* requestFailed=*/ true
- );
- }
-
- _updateRequestToast(numPending: number, requestFailed?: boolean) {
- const message = this._getSavingMessage(numPending, requestFailed);
- this.draftToastTask = debounce(
- this.draftToastTask,
- () => {
- // Note: the event is fired on the body rather than this element because
- // this element may not be attached by the time this executes, in which
- // case the event would not bubble.
- fireAlert(document.body, message);
- },
- TOAST_DEBOUNCE_INTERVAL
- );
- }
-
- _handleDraftFailure() {
- this.$.container.classList.add('unableToSave');
- this._unableToSave = true;
- this._handleFailedDraftRequest();
- }
-
- _saveDraft(draft?: UIComment) {
- if (!draft || this.changeNum === undefined || this.patchNum === undefined) {
- throw new Error('undefined draft or changeNum or patchNum');
- }
- this._showStartRequest();
- return this.restApiService
- .saveDiffDraft(this.changeNum, this.patchNum, draft)
- .then(result => {
- if (result.ok) {
- // remove
- this._unableToSave = false;
- this.$.container.classList.remove('unableToSave');
- this._showEndRequest();
- } else {
- this._handleDraftFailure();
- }
- return result;
- })
- .catch(err => {
- this._handleDraftFailure();
- throw err;
- });
- }
-
- _deleteDraft(draft: UIComment) {
- const changeNum = this.changeNum;
- const patchNum = this.patchNum;
- if (changeNum === undefined || patchNum === undefined) {
- throw new Error('undefined changeNum or patchNum');
- }
- fireAlert(this, 'Discarding draft...');
- const draftID = draft.id;
- if (!draftID) throw new Error('Missing id in comment draft.');
- return this.restApiService
- .deleteDiffDraft(changeNum, patchNum, {id: draftID})
- .then(result => {
- if (result.ok) {
- fire(this, 'show-alert', {
- message: 'Draft Discarded',
- action: 'Undo',
- callback: () =>
- this.commentsService.restoreDraft(changeNum, patchNum, draftID),
- });
- }
- return result;
- });
- }
-
- _getPatchNum(): PatchSetNum {
- const patchNum = this.isOnParent()
- ? ('PARENT' as BasePatchSetNum)
- : this.patchNum;
- if (patchNum === undefined) throw new Error('patchNum undefined');
- return patchNum;
- }
-
- @observe('changeNum', 'patchNum', 'comment')
- _loadLocalDraft(
- changeNum: number,
- patchNum?: PatchSetNum,
- comment?: UIComment
- ) {
- // Polymer 2: check for undefined
- if ([changeNum, patchNum, comment].includes(undefined)) {
- return;
- }
-
- // Only apply local drafts to comments that are drafts and are currently
- // being edited.
- if (
- !comment ||
- !comment.path ||
- comment.message ||
- !isDraft(comment) ||
- !comment.__editing
- ) {
- return;
- }
-
- const draft = this.storage.getDraftComment({
- changeNum,
- patchNum: this._getPatchNum(),
- path: comment.path,
- line: comment.line,
- range: comment.range,
+ private handleAnchorClick() {
+ assertIsDefined(this.comment, 'comment');
+ fire(this, 'comment-anchor-tap', {
+ number: this.comment.line || FILE,
+ side: this.comment?.side,
});
+ }
- if (draft) {
- this._messageText = draft.message || '';
+ private handleFix() {
+ // Handled by <gr-comment-thread>.
+ fire(this, 'create-fix-comment', this.getEventPayload());
+ }
+
+ private handleShowFix() {
+ // Handled top-level in the diff and change view components.
+ fire(this, 'open-fix-preview', this.getEventPayload());
+ }
+
+ // private, but visible for testing
+ cancel() {
+ assertIsDefined(this.comment, 'comment');
+ if (!isDraftOrUnsaved(this.comment)) {
+ throw new Error('only unsaved and draft comments are editable');
+ }
+ this.messageText = this.originalMessage;
+ this.unresolved = this.originalUnresolved;
+ this.save();
+ }
+
+ async autoSave() {
+ if (this.saving || this.autoSaving) return;
+ if (!this.editing || !this.comment) return;
+ if (!isDraftOrUnsaved(this.comment)) return;
+ const messageToSave = this.messageText.trimEnd();
+ if (messageToSave === '') return;
+ if (messageToSave === this.comment.message) return;
+
+ try {
+ this.autoSaving = this.rawSave(messageToSave, {showToast: false});
+ await this.autoSaving;
+ } finally {
+ this.autoSaving = undefined;
}
}
- _handleToggleResolved() {
- this.reporting.recordDraftInteraction();
- this.resolved = !this.resolved;
- // Modify payload instead of this.comment, as this.comment is passed from
- // the parent by ref.
- const payload = this._getEventPayload();
- if (!payload.comment) {
- throw new Error('comment not defined in payload');
+ async discard() {
+ this.messageText = '';
+ await this.save();
+ }
+
+ async save() {
+ if (!isDraftOrUnsaved(this.comment)) throw new Error('not a draft');
+
+ try {
+ this.saving = true;
+ this.unableToSave = false;
+ if (this.autoSaving) {
+ this.comment = await this.autoSaving;
+ }
+ // Depending on whether `messageToSave` is empty we treat this either as
+ // a discard or a save action.
+ const messageToSave = this.messageText.trimEnd();
+ if (messageToSave === '') {
+ // Don't try to discard UnsavedInfo. Nothing to do then.
+ if (this.comment.id) {
+ await this.getCommentsModel().discardDraft(this.comment.id);
+ }
+ } else {
+ // No need to make a backend call when nothing has changed.
+ if (
+ messageToSave !== this.comment?.message ||
+ this.unresolved !== this.comment.unresolved
+ ) {
+ await this.rawSave(messageToSave, {showToast: true});
+ }
+ }
+ this.editing = false;
+ } catch (e) {
+ this.unableToSave = true;
+ throw e;
+ } finally {
+ this.saving = false;
}
- payload.comment.unresolved = !this.$.resolvedCheckbox.checked;
- this.dispatchEvent(
- new CustomEvent('comment-update', {
- detail: payload,
- composed: true,
- bubbles: true,
- })
+ }
+
+ /** For sharing between save() and autoSave(). */
+ private rawSave(message: string, options: {showToast: boolean}) {
+ if (!isDraftOrUnsaved(this.comment)) throw new Error('not a draft');
+ return this.getCommentsModel().saveDraft(
+ {
+ ...this.comment,
+ message,
+ unresolved: this.unresolved,
+ },
+ options.showToast
);
+ }
+
+ private handleToggleResolved() {
+ this.unresolved = !this.unresolved;
if (!this.editing) {
- // Save the resolved state immediately.
- this.save(payload.comment);
+ // messageText is only assigned a value if the comment reaches editing
+ // state, however it is possible that the user toggles the resolved state
+ // without editing the comment in which case we assign the correct value
+ // to messageText here
+ this.messageText = this.comment?.message ?? '';
+ this.save();
}
}
- _handleCommentDelete() {
- this._openOverlay(this.confirmDeleteOverlay);
+ private async openDeleteCommentOverlay() {
+ this.showConfirmDeleteOverlay = true;
+ await this.updateComplete;
+ await this.confirmDeleteOverlay?.open();
}
- _handleCancelDeleteComment() {
- this._closeOverlay(this.confirmDeleteOverlay);
+ private closeDeleteCommentOverlay() {
+ this.showConfirmDeleteOverlay = false;
+ this.confirmDeleteOverlay?.remove();
+ this.confirmDeleteOverlay?.close();
}
- _openOverlay(overlay?: GrOverlay | null) {
- if (!overlay) {
- return Promise.reject(new Error('undefined overlay'));
- }
- getRootElement().appendChild(overlay);
- return overlay.open();
- }
-
- _computeHideRunDetails(comment: UIComment | undefined, collapsed: boolean) {
- if (!comment) return true;
- if (!isRobot(comment)) return true;
- return !comment.url || collapsed;
- }
-
- _closeOverlay(overlay?: GrOverlay | null) {
- if (overlay) {
- getRootElement().removeChild(overlay);
- overlay.close();
- }
- }
-
- _handleConfirmDeleteComment() {
+ /**
+ * Deleting a *published* comment is an admin feature. It means more than just
+ * discarding a draft.
+ *
+ * TODO: Also move this into the comments-service.
+ * TODO: Figure out a good reloading strategy when deleting was successful.
+ * `this.comment = newComment` does not seem sufficient.
+ */
+ // private, but visible for testing
+ handleConfirmDeleteComment() {
const dialog = this.confirmDeleteOverlay?.querySelector(
'#confirmDeleteComment'
) as GrConfirmDeleteCommentDialog | null;
if (!dialog || !dialog.message) {
throw new Error('missing confirm delete dialog');
}
- if (
- !this.comment ||
- !this.comment.id ||
- this.changeNum === undefined ||
- this.patchNum === undefined
- ) {
- throw new Error('undefined comment or id or changeNum or patchNum');
+ assertIsDefined(this.changeNum, 'changeNum');
+ assertIsDefined(this.comment, 'comment');
+ assertIsDefined(this.comment.patch_set, 'comment.patch_set');
+ if (isDraftOrUnsaved(this.comment)) {
+ throw new Error('Admin deletion is only for published comments.');
}
this.restApiService
.deleteComment(
this.changeNum,
- this.patchNum,
+ this.comment.patch_set,
this.comment.id,
dialog.message
)
.then(newComment => {
- this._handleCancelDeleteComment();
+ this.closeDeleteCommentOverlay();
this.comment = newComment;
});
}
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
deleted file mode 100644
index b77c4b2..0000000
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
+++ /dev/null
@@ -1,497 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- display: block;
- font-family: var(--font-family);
- padding: var(--spacing-m);
- }
- :host([collapsed]) {
- padding: var(--spacing-s) var(--spacing-m);
- }
- :host([disabled]) {
- pointer-events: none;
- }
- :host([disabled]) .actions,
- :host([disabled]) .robotActions,
- :host([disabled]) .date {
- opacity: 0.5;
- }
- :host([discarding]) {
- display: none;
- }
- .body {
- padding-top: var(--spacing-m);
- }
- .header {
- align-items: center;
- cursor: pointer;
- display: flex;
- }
- .headerLeft > span {
- font-weight: var(--font-weight-bold);
- }
- .headerMiddle {
- color: var(--deemphasized-text-color);
- flex: 1;
- overflow: hidden;
- }
- .draftLabel,
- .draftTooltip {
- color: var(--deemphasized-text-color);
- display: none;
- }
- .date {
- justify-content: flex-end;
- text-align: right;
- white-space: nowrap;
- }
- span.date {
- color: var(--deemphasized-text-color);
- }
- span.date:hover {
- text-decoration: underline;
- }
- .actions,
- .robotActions {
- display: flex;
- justify-content: flex-end;
- padding-top: 0;
- }
- .robotActions {
- /* Better than the negative margin would be to remove the gr-button
- * padding, but then we would also need to fix the buttons that are
- * inserted by plugins. :-/ */
- margin: 4px 0 -4px;
- }
- .action {
- margin-left: var(--spacing-l);
- }
- .rightActions {
- display: flex;
- justify-content: flex-end;
- }
- .rightActions gr-button {
- --gr-button-padding: 0 var(--spacing-s);
- }
- .editMessage {
- display: none;
- margin: var(--spacing-m) 0;
- width: 100%;
- }
- .container:not(.draft) .actions .hideOnPublished {
- display: none;
- }
- .draft .reply,
- .draft .quote,
- .draft .ack,
- .draft .done {
- display: none;
- }
- .draft .draftLabel,
- .draft .draftTooltip {
- display: inline;
- }
- .draft:not(.editing):not(.unableToSave) .save,
- .draft:not(.editing) .cancel {
- display: none;
- }
- .editing .message,
- .editing .reply,
- .editing .quote,
- .editing .ack,
- .editing .done,
- .editing .edit,
- .editing .discard,
- .editing .unresolved {
- display: none;
- }
- .editing .editMessage {
- display: block;
- }
- .show-hide {
- margin-left: var(--spacing-s);
- }
- .robotId {
- color: var(--deemphasized-text-color);
- margin-bottom: var(--spacing-m);
- }
- .robotRun {
- margin-left: var(--spacing-m);
- }
- .robotRunLink {
- margin-left: var(--spacing-m);
- }
- input.show-hide {
- display: none;
- }
- label.show-hide {
- cursor: pointer;
- display: block;
- }
- label.show-hide iron-icon {
- vertical-align: top;
- }
- #container .collapsedContent {
- display: none;
- }
- #container.collapsed .body {
- padding-top: 0;
- }
- #container.collapsed .collapsedContent {
- display: block;
- overflow: hidden;
- padding-left: var(--spacing-m);
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- #container.collapsed #deleteBtn,
- #container.collapsed .date,
- #container.collapsed .actions,
- #container.collapsed gr-formatted-text,
- #container.collapsed gr-textarea,
- #container.collapsed .respectfulReviewTip {
- display: none;
- }
- .resolve,
- .unresolved {
- align-items: center;
- display: flex;
- flex: 1;
- margin: 0;
- }
- .resolve label {
- color: var(--comment-text-color);
- }
- gr-dialog .main {
- display: flex;
- flex-direction: column;
- width: 100%;
- }
- #deleteBtn {
- display: none;
- --gr-button-text-color: var(--deemphasized-text-color);
- --gr-button-padding: 0;
- }
- #deleteBtn.showDeleteButtons {
- display: block;
- }
-
- /** Disable select for the caret and actions */
- .actions,
- .show-hide {
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- }
-
- .respectfulReviewTip {
- justify-content: space-between;
- display: flex;
- padding: var(--spacing-m);
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius);
- margin-bottom: var(--spacing-m);
- }
- .respectfulReviewTip div {
- display: flex;
- }
- .respectfulReviewTip div iron-icon {
- margin-right: var(--spacing-s);
- }
- .respectfulReviewTip a {
- white-space: nowrap;
- margin-right: var(--spacing-s);
- padding-left: var(--spacing-m);
- text-decoration: none;
- }
- .pointer {
- cursor: pointer;
- }
- .patchset-text {
- color: var(--deemphasized-text-color);
- margin-left: var(--spacing-s);
- }
- .headerLeft gr-account-label {
- --account-max-length: 130px;
- width: 150px;
- }
- .headerLeft gr-account-label::part(gr-account-label-text) {
- font-weight: var(--font-weight-bold);
- }
- .draft gr-account-label {
- width: unset;
- }
- .portedMessage {
- margin: 0 var(--spacing-m);
- }
- .link-icon {
- cursor: pointer;
- }
- </style>
- <div id="container" class="container">
- <div class="header" id="header" on-click="_handleToggleCollapsed">
- <div class="headerLeft">
- <template is="dom-if" if="[[comment.robot_id]]">
- <span class="robotName"> [[comment.robot_id]] </span>
- </template>
- <template is="dom-if" if="[[!comment.robot_id]]">
- <gr-account-label
- account="[[_getAuthor(comment, _selfAccount)]]"
- class$="[[_computeAccountLabelClass(draft)]]"
- hideStatus
- >
- </gr-account-label>
- </template>
- <template is="dom-if" if="[[showPortedComment]]">
- <a href="[[_getUrlForComment(comment)]]"
- ><span class="portedMessage" on-click="_handlePortedMessageClick"
- >From patchset [[comment.patch_set]]</span
- ></a
- >
- </template>
- <gr-tooltip-content
- class="draftTooltip"
- has-tooltip
- title="[[_computeDraftTooltip(_unableToSave)]]"
- max-width="20em"
- show-icon
- >
- <span class="draftLabel">[[_computeDraftText(_unableToSave)]]</span>
- </gr-tooltip-content>
- </div>
- <div class="headerMiddle">
- <span class="collapsedContent">[[comment.message]]</span>
- </div>
- <div
- hidden$="[[_computeHideRunDetails(comment, collapsed)]]"
- class="runIdMessage message"
- >
- <div class="runIdInformation">
- <a class="robotRunLink" href$="[[comment.url]]">
- <span class="robotRun link">Run Details</span>
- </a>
- </div>
- </div>
- <gr-button
- id="deleteBtn"
- title="Delete Comment"
- link=""
- class$="action delete [[_computeDeleteButtonClass(_isAdmin, draft)]]"
- hidden$="[[isRobotComment]]"
- on-click="_handleCommentDelete"
- >
- <iron-icon id="icon" icon="gr-icons:delete"></iron-icon>
- </gr-button>
- <template is="dom-if" if="[[showPatchset]]">
- <span class="patchset-text"> Patchset [[patchNum]]</span>
- </template>
- <span class="separator"></span>
- <template is="dom-if" if="[[comment.updated]]">
- <span class="date" tabindex="0" on-click="_handleAnchorClick">
- <gr-date-formatter
- withTooltip
- date-str="[[comment.updated]]"
- ></gr-date-formatter>
- </span>
- </template>
- <div class="show-hide" tabindex="0">
- <label
- class="show-hide"
- aria-label$="[[_computeShowHideAriaLabel(collapsed)]]"
- >
- <input
- type="checkbox"
- class="show-hide"
- checked$="[[collapsed]]"
- on-change="_handleToggleCollapsed"
- />
- <iron-icon id="icon" icon="[[_computeShowHideIcon(collapsed)]]">
- </iron-icon>
- </label>
- </div>
- </div>
- <div class="body">
- <template is="dom-if" if="[[isRobotComment]]">
- <div class="robotId" hidden$="[[collapsed]]">
- [[comment.author.name]]
- </div>
- </template>
- <template is="dom-if" if="[[editing]]">
- <gr-textarea
- id="editTextarea"
- class="editMessage"
- autocomplete="on"
- code=""
- disabled="{{disabled}}"
- rows="4"
- text="{{_messageText}}"
- ></gr-textarea>
- <template
- is="dom-if"
- if="[[_computeVisibilityOfTip(_showRespectfulTip, _respectfulTipDismissed)]]"
- >
- <div class="respectfulReviewTip">
- <div>
- <gr-tooltip-content
- has-tooltip
- title="Tips for respectful code reviews."
- >
- <iron-icon
- class="pointer"
- icon="gr-icons:lightbulb-outline"
- ></iron-icon>
- </gr-tooltip-content>
- [[_respectfulReviewTip]]
- </div>
- <div>
- <a
- tabindex="-1"
- on-click="_onRespectfulReadMoreClick"
- href="https://testing.googleblog.com/2019/11/code-health-respectful-reviews-useful.html"
- target="_blank"
- >
- Read more
- </a>
- <a
- tabindex="-1"
- class="close pointer"
- on-click="_dismissRespectfulTip"
- >Not helpful</a
- >
- </div>
- </div>
- </template>
- </template>
- <!--The message class is needed to ensure selectability from
- gr-diff-selection.-->
- <gr-formatted-text
- class="message"
- content="[[comment.message]]"
- no-trailing-margin="[[!comment.__draft]]"
- config="[[projectConfig.commentlinks]]"
- ></gr-formatted-text>
- <div class="actions humanActions" hidden$="[[!_showHumanActions]]">
- <div class="action resolve hideOnPublished">
- <label>
- <input
- type="checkbox"
- id="resolvedCheckbox"
- checked="[[resolved]]"
- on-change="_handleToggleResolved"
- />
- Resolved
- </label>
- </div>
- <template is="dom-if" if="[[draft]]">
- <div class="rightActions">
- <template is="dom-if" if="[[hasPublishedComment(comments)]]">
- <iron-icon
- class="link-icon"
- on-click="handleCopyLink"
- class="copy"
- title="Copy link to this comment"
- icon="gr-icons:link"
- role="button"
- tabindex="0"
- >
- </iron-icon>
- </template>
- <gr-button
- link=""
- class="action cancel hideOnPublished"
- on-click="_handleCancel"
- >Cancel</gr-button
- >
- <gr-button
- link=""
- class="action discard hideOnPublished"
- on-click="_handleDiscard"
- >Discard</gr-button
- >
- <gr-button
- link=""
- class="action edit hideOnPublished"
- on-click="_handleEdit"
- >Edit</gr-button
- >
- <gr-button
- link=""
- disabled$="[[_computeSaveDisabled(_messageText, comment, resolved)]]"
- class="action save hideOnPublished"
- on-click="_handleSave"
- >Save</gr-button
- >
- </div>
- </template>
- </div>
- <div class="robotActions" hidden$="[[!_showRobotActions]]">
- <template is="dom-if" if="[[hasPublishedComment(comments)]]">
- <iron-icon
- class="link-icon"
- on-click="handleCopyLink"
- class="copy"
- title="Copy link to this comment"
- icon="gr-icons:link"
- role="button"
- tabindex="0"
- >
- </iron-icon>
- </template>
- <template is="dom-if" if="[[isRobotComment]]">
- <gr-endpoint-decorator name="robot-comment-controls">
- <gr-endpoint-param name="comment" value="[[comment]]">
- </gr-endpoint-param>
- </gr-endpoint-decorator>
- <gr-button
- link=""
- secondary=""
- class="action show-fix"
- hidden$="[[_hasNoFix(comment)]]"
- on-click="_handleShowFix"
- >
- Show Fix
- </gr-button>
- <template is="dom-if" if="[[!_hasHumanReply]]">
- <gr-button
- link=""
- class="action fix"
- on-click="_handleFix"
- disabled="[[robotButtonDisabled]]"
- >
- Please Fix
- </gr-button>
- </template>
- </template>
- </div>
- </div>
- </div>
- <template is="dom-if" if="[[_enableOverlay]]">
- <gr-overlay id="confirmDeleteOverlay" with-backdrop="">
- <gr-confirm-delete-comment-dialog
- id="confirmDeleteComment"
- on-confirm="_handleConfirmDeleteComment"
- on-cancel="_handleCancelDeleteComment"
- >
- </gr-confirm-delete-comment-dialog>
- </gr-overlay>
- </template>
-`;
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 00edc07..d3a1007 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
@@ -14,1501 +14,679 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
import '../../../test/common-test-setup-karma';
import './gr-comment';
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-import {GrComment, __testOnly_UNSAVED_MESSAGE} from './gr-comment';
-import {SpecialFilePath, CommentSide} from '../../../constants/constants';
+import {AUTO_SAVE_DEBOUNCE_DELAY_MS, GrComment} from './gr-comment';
import {
queryAndAssert,
stubRestApi,
stubStorage,
- spyStorage,
query,
- isVisible,
- stubReporting,
+ pressKey,
+ listenOnce,
mockPromise,
+ waitUntilCalled,
+ dispatch,
+ MockPromise,
} from '../../../test/test-utils';
import {
AccountId,
EmailAddress,
- FixId,
NumericChangeId,
- ParsedJSON,
PatchSetNum,
- RobotId,
- RobotRunId,
Timestamp,
UrlEncodedCommentId,
} from '../../../types/common';
-import {
- pressAndReleaseKeyOn,
- tap,
-} from '@polymer/iron-test-helpers/mock-interactions';
+import {tap} from '@polymer/iron-test-helpers/mock-interactions';
import {
createComment,
createDraft,
createFixSuggestionInfo,
+ createRobotComment,
+ createUnsaved,
} from '../../../test/test-data-generators';
-import {Timer} from '../../../services/gr-reporting/gr-reporting';
-import {SinonFakeTimers, SinonStubbedMember} from 'sinon';
-import {CreateFixCommentEvent} from '../../../types/events';
-import {DraftInfo, UIRobot} from '../../../utils/comment-util';
-import {MockTimer} from '../../../services/gr-reporting/gr-reporting_mock';
+import {
+ CreateFixCommentEvent,
+ OpenFixPreviewEventDetail,
+} from '../../../types/events';
import {GrConfirmDeleteCommentDialog} from '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
-
-const basicFixture = fixtureFromElement('gr-comment');
-
-const draftFixture = fixtureFromTemplate(html`
- <gr-comment draft="true"></gr-comment>
-`);
+import {DraftInfo} from '../../../utils/comment-util';
+import {assertIsDefined} from '../../../utils/common-util';
+import {Modifier} from '../../../utils/dom-util';
+import {SinonStub} from 'sinon';
suite('gr-comment tests', () => {
- suite('basic tests', () => {
- let element: GrComment;
+ let element: GrComment;
- let openOverlaySpy: sinon.SinonSpy;
+ setup(() => {
+ element = fixtureFromElement('gr-comment').instantiate();
+ element.account = {
+ email: 'dhruvsri@google.com' as EmailAddress,
+ name: 'Dhruv Srivastava',
+ _account_id: 1083225 as AccountId,
+ avatars: [{url: 'abc', height: 32, width: 32}],
+ registered_on: '123' as Timestamp,
+ };
+ element.showPatchset = true;
+ element.getRandomInt = () => 1;
+ element.comment = {
+ ...createComment(),
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com' as EmailAddress,
+ },
+ id: 'baf0414d_60047215' as UrlEncodedCommentId,
+ line: 5,
+ message: 'This is the test comment message.',
+ updated: '2015-12-08 19:48:33.843000000' as Timestamp,
+ };
+ });
- setup(() => {
- stubRestApi('getAccount').returns(
- Promise.resolve({
- email: 'dhruvsri@google.com' as EmailAddress,
- name: 'Dhruv Srivastava',
- _account_id: 1083225 as AccountId,
- avatars: [{url: 'abc', height: 32, width: 32}],
- registered_on: '123' as Timestamp,
- })
- );
- element = basicFixture.instantiate();
- element.comment = {
- ...createComment(),
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com' as EmailAddress,
- },
- id: 'baf0414d_60047215' as UrlEncodedCommentId,
- line: 5,
- message: 'is this a crossover episode!?',
- updated: '2015-12-08 19:48:33.843000000' as Timestamp,
- };
-
- openOverlaySpy = sinon.spy(element, '_openOverlay');
+ suite('DOM rendering', () => {
+ test('renders collapsed', async () => {
+ element.initiallyCollapsed = true;
+ await element.updateComplete;
+ expect(element).shadowDom.to.equal(`
+ <div class="container" id="container">
+ <div class="header" id="header">
+ <div class="headerLeft">
+ <gr-account-label deselected="" hidestatus=""></gr-account-label>
+ </div>
+ <div class="headerMiddle">
+ <span class="collapsedContent">
+ This is the test comment message.
+ </span>
+ </div>
+ <span class="patchset-text">Patchset 1</span>
+ <div class="show-hide" tabindex="0">
+ <label aria-label="Expand" class="show-hide">
+ <input checked="" class="show-hide" type="checkbox">
+ <iron-icon id="icon" icon="gr-icons:expand-more"></iron-icon>
+ </label>
+ </div>
+ </div>
+ <div class="body"></div>
+ </div>
+ `);
});
- teardown(() => {
- openOverlaySpy.getCalls().forEach(call => {
- call.args[0].remove();
- });
+ test('renders expanded', async () => {
+ element.initiallyCollapsed = false;
+ await element.updateComplete;
+ expect(element).shadowDom.to.equal(`
+ <div class="container" id="container">
+ <div class="header" id="header">
+ <div class="headerLeft">
+ <gr-account-label deselected="" hidestatus=""></gr-account-label>
+ </div>
+ <div class="headerMiddle"></div>
+ <span class="patchset-text">Patchset 1</span>
+ <span class="separator"></span>
+ <span class="date" tabindex="0">
+ <gr-date-formatter withtooltip=""></gr-date-formatter>
+ </span>
+ <div class="show-hide" tabindex="0">
+ <label aria-label="Collapse" class="show-hide">
+ <input class="show-hide" type="checkbox">
+ <iron-icon id="icon" icon="gr-icons:expand-less"></iron-icon>
+ </label>
+ </div>
+ </div>
+ <div class="body">
+ <gr-formatted-text class="message" notrailingmargin=""></gr-formatted-text>
+ </div>
+ </div>
+ `);
});
- test('collapsible comments', () => {
- // When a comment (not draft) is loaded, it should be collapsed
- assert.isTrue(element.collapsed);
- assert.isFalse(
- isVisible(queryAndAssert(element, 'gr-formatted-text')),
- 'gr-formatted-text is not visible'
- );
- assert.isFalse(
- isVisible(queryAndAssert(element, '.actions')),
- 'actions are not visible'
- );
- assert.isNotOk(element.textarea, 'textarea is not visible');
-
- // The header middle content is only visible when comments are collapsed.
- // It shows the message in a condensed way, and limits to a single line.
- assert.isTrue(
- isVisible(queryAndAssert(element, '.collapsedContent')),
- 'header middle content is visible'
- );
-
- // When the header row is clicked, the comment should expand
- tap(element.$.header);
- assert.isFalse(element.collapsed);
- assert.isTrue(
- isVisible(queryAndAssert(element, 'gr-formatted-text')),
- 'gr-formatted-text is visible'
- );
- assert.isTrue(
- isVisible(queryAndAssert(element, '.actions')),
- 'actions are visible'
- );
- assert.isNotOk(element.textarea, 'textarea is not visible');
- assert.isFalse(
- isVisible(queryAndAssert(element, '.collapsedContent')),
- 'header middle content is not visible'
- );
+ test('renders expanded robot', async () => {
+ element.initiallyCollapsed = false;
+ element.comment = createRobotComment();
+ await element.updateComplete;
+ expect(element).shadowDom.to.equal(`
+ <div class="container" id="container">
+ <div class="header" id="header">
+ <div class="headerLeft">
+ <span class="robotName">robot-id-123</span>
+ </div>
+ <div class="headerMiddle"></div>
+ <span class="patchset-text">Patchset 1</span>
+ <span class="separator"></span>
+ <span class="date" tabindex="0">
+ <gr-date-formatter withtooltip=""></gr-date-formatter>
+ </span>
+ <div class="show-hide" tabindex="0">
+ <label aria-label="Collapse" class="show-hide">
+ <input class="show-hide" type="checkbox">
+ <iron-icon id="icon" icon="gr-icons:expand-less"></iron-icon>
+ </label>
+ </div>
+ </div>
+ <div class="body">
+ <div class="robotId"></div>
+ <gr-formatted-text class="message" notrailingmargin=""></gr-formatted-text>
+ <div class="robotActions">
+ <iron-icon class="copy link-icon" icon="gr-icons:link" role="button" tabindex="0"
+ title="Copy link to this comment">
+ </iron-icon>
+ <gr-endpoint-decorator name="robot-comment-controls">
+ <gr-endpoint-param name="comment"></gr-endpoint-param>
+ </gr-endpoint-decorator>
+ <gr-button aria-disabled="false" class="action show-fix" link="" role="button" secondary="" tabindex="0">
+ Show Fix
+ </gr-button>
+ <gr-button aria-disabled="false" class="action fix" link="" role="button" tabindex="0">
+ Please Fix
+ </gr-button>
+ </div>
+ </div>
+ </div>
+ `);
});
- test('clicking on date link fires event', () => {
- element.side = 'PARENT';
- const stub = sinon.stub();
- element.addEventListener('comment-anchor-tap', stub);
- flush();
- const dateEl = queryAndAssert(element, '.date');
- assert.ok(dateEl);
- tap(dateEl);
-
- assert.isTrue(stub.called);
- assert.deepEqual(stub.lastCall.args[0].detail, {
- side: element.side,
- number: element.comment!.line,
- });
+ test('renders expanded admin', async () => {
+ element.initiallyCollapsed = false;
+ element.isAdmin = true;
+ await element.updateComplete;
+ expect(queryAndAssert(element, 'gr-button.delete')).dom.to.equal(`
+ <gr-button
+ aria-disabled="false"
+ class="action delete"
+ id="deleteBtn"
+ link=""
+ role="button"
+ tabindex="0"
+ title="Delete Comment"
+ >
+ <iron-icon icon="gr-icons:delete" id="icon"></iron-icon>
+ </gr-button>
+ `);
});
- test('message is not retrieved from storage when missing path', async () => {
- const storageStub = stubStorage('getDraftComment');
- const loadSpy = sinon.spy(element, '_loadLocalDraft');
-
- element.changeNum = 1 as NumericChangeId;
- element.patchNum = 1 as PatchSetNum;
- element.comment = {
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com' as EmailAddress,
- },
- line: 5,
- };
- await flush();
- assert.isTrue(loadSpy.called);
- assert.isFalse(storageStub.called);
+ test('renders draft', async () => {
+ element.initiallyCollapsed = false;
+ (element.comment as DraftInfo).__draft = true;
+ await element.updateComplete;
+ expect(element).shadowDom.to.equal(`
+ <div class="container draft" id="container">
+ <div class="header" id="header">
+ <div class="headerLeft">
+ <gr-account-label class="draft" deselected="" hidestatus=""></gr-account-label>
+ <gr-tooltip-content
+ class="draftTooltip" has-tooltip="" max-width="20em" show-icon=""
+ title="This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'a' key."
+ >
+ <span class="draftLabel">DRAFT</span>
+ </gr-tooltip-content>
+ </div>
+ <div class="headerMiddle"></div>
+ <span class="patchset-text">Patchset 1</span>
+ <span class="separator"></span>
+ <span class="date" tabindex="0">
+ <gr-date-formatter withtooltip=""></gr-date-formatter>
+ </span>
+ <div class="show-hide" tabindex="0">
+ <label aria-label="Collapse" class="show-hide">
+ <input class="show-hide" type="checkbox">
+ <iron-icon id="icon" icon="gr-icons:expand-less"></iron-icon>
+ </label>
+ </div>
+ </div>
+ <div class="body">
+ <gr-formatted-text class="message"></gr-formatted-text>
+ <div class="actions">
+ <div class="action resolve">
+ <label>
+ <input checked="" id="resolvedCheckbox" type="checkbox">
+ Resolved
+ </label>
+ </div>
+ <div class="rightActions">
+ <gr-button aria-disabled="false" class="action discard" link="" role="button" tabindex="0">
+ Discard
+ </gr-button>
+ <gr-button aria-disabled="false" class="action edit" link="" role="button" tabindex="0">
+ Edit
+ </gr-button>
+ </div>
+ </div>
+ </div>
+ </div>
+ `);
});
- test('message is not retrieved from storage when message present', async () => {
- const storageStub = stubStorage('getDraftComment');
- const loadSpy = sinon.spy(element, '_loadLocalDraft');
-
- element.changeNum = 1 as NumericChangeId;
- element.patchNum = 1 as PatchSetNum;
- element.comment = {
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com' as EmailAddress,
- },
- message: 'This is a message',
- line: 5,
- path: 'test',
- __editing: true,
- __draft: true,
- };
- await flush();
- assert.isTrue(loadSpy.called);
- assert.isFalse(storageStub.called);
- });
-
- test('message is retrieved from storage for drafts in edit', async () => {
- const storageStub = stubStorage('getDraftComment');
- const loadSpy = sinon.spy(element, '_loadLocalDraft');
-
- element.changeNum = 1 as NumericChangeId;
- element.patchNum = 1 as PatchSetNum;
- element.comment = {
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com' as EmailAddress,
- },
- line: 5,
- path: 'test',
- __editing: true,
- __draft: true,
- };
- await flush();
- assert.isTrue(loadSpy.called);
- assert.isTrue(storageStub.called);
- });
-
- test('comment message sets messageText only when empty', () => {
- element.changeNum = 1 as NumericChangeId;
- element.patchNum = 1 as PatchSetNum;
- element._messageText = '';
- element.comment = {
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com' as EmailAddress,
- },
- line: 5,
- path: 'test',
- __editing: true,
- __draft: true,
- message: 'hello world',
- };
- // messageText was empty so overwrite the message now
- assert.equal(element._messageText, 'hello world');
-
- element.comment!.message = 'new message';
- // messageText was already set so do not overwrite it
- assert.equal(element._messageText, 'hello world');
- });
-
- test('comment message sets messageText when not edited', () => {
- element.changeNum = 1 as NumericChangeId;
- element.patchNum = 1 as PatchSetNum;
- element._messageText = 'Some text';
- element.comment = {
- author: {
- name: 'Mr. Peanutbutter',
- email: 'tenn1sballchaser@aol.com' as EmailAddress,
- },
- line: 5,
- path: 'test',
- __editing: false,
- __draft: true,
- message: 'hello world',
- };
- // messageText was empty so overwrite the message now
- assert.equal(element._messageText, 'hello world');
-
- element.comment!.message = 'new message';
- // messageText was already set so do not overwrite it
- assert.equal(element._messageText, 'hello world');
- });
-
- test('_getPatchNum', () => {
- element.side = 'PARENT';
- element.patchNum = 1 as PatchSetNum;
- assert.equal(element._getPatchNum(), 'PARENT' as PatchSetNum);
- element.side = 'REVISION';
- assert.equal(element._getPatchNum(), 1 as PatchSetNum);
- });
-
- test('comment expand and collapse', () => {
- element.collapsed = true;
- assert.isFalse(
- isVisible(queryAndAssert(element, 'gr-formatted-text')),
- 'gr-formatted-text is not visible'
- );
- assert.isFalse(
- isVisible(queryAndAssert(element, '.actions')),
- 'actions are not visible'
- );
- assert.isNotOk(element.textarea, 'textarea is not visible');
- assert.isTrue(
- isVisible(queryAndAssert(element, '.collapsedContent')),
- 'header middle content is visible'
- );
-
- element.collapsed = false;
- assert.isFalse(element.collapsed);
- assert.isTrue(
- isVisible(queryAndAssert(element, 'gr-formatted-text')),
- 'gr-formatted-text is visible'
- );
- assert.isTrue(
- isVisible(queryAndAssert(element, '.actions')),
- 'actions are visible'
- );
- assert.isNotOk(element.textarea, 'textarea is not visible');
- assert.isFalse(
- isVisible(queryAndAssert(element, '.collapsedContent')),
- 'header middle content is is not visible'
- );
- });
-
- suite('while editing', () => {
- let handleCancelStub: sinon.SinonStub;
- let handleSaveStub: sinon.SinonStub;
- setup(() => {
- element.editing = true;
- element._messageText = 'test';
- handleCancelStub = sinon.stub(element, '_handleCancel');
- handleSaveStub = sinon.stub(element, '_handleSave');
- flush();
- });
-
- suite('when text is empty', () => {
- setup(() => {
- element._messageText = '';
- element.comment = {};
- });
-
- test('esc closes comment when text is empty', () => {
- pressAndReleaseKeyOn(element.textarea!, 27, null, 'Escape');
- assert.isTrue(handleCancelStub.called);
- });
-
- test('ctrl+enter does not save', () => {
- pressAndReleaseKeyOn(element.textarea!, 13, 'ctrl', 'Enter');
- assert.isFalse(handleSaveStub.called);
- });
-
- test('meta+enter does not save', () => {
- pressAndReleaseKeyOn(element.textarea!, 13, 'meta', 'Enter');
- assert.isFalse(handleSaveStub.called);
- });
-
- test('ctrl+s does not save', () => {
- pressAndReleaseKeyOn(element.textarea!, 83, 'ctrl', 's');
- assert.isFalse(handleSaveStub.called);
- });
- });
-
- test('esc does not close comment that has content', () => {
- pressAndReleaseKeyOn(element.textarea!, 27, null, 'Escape');
- assert.isFalse(handleCancelStub.called);
- });
-
- test('ctrl+enter saves', () => {
- pressAndReleaseKeyOn(element.textarea!, 13, 'ctrl', 'Enter');
- assert.isTrue(handleSaveStub.called);
- });
-
- test('meta+enter saves', () => {
- pressAndReleaseKeyOn(element.textarea!, 13, 'meta', 'Enter');
- assert.isTrue(handleSaveStub.called);
- });
-
- test('ctrl+s saves', () => {
- pressAndReleaseKeyOn(element.textarea!, 83, 'ctrl', 's');
- assert.isTrue(handleSaveStub.called);
- });
- });
-
- test('delete comment button for non-admins is hidden', () => {
- element._isAdmin = false;
- assert.isFalse(
- queryAndAssert(element, '.action.delete').classList.contains(
- 'showDeleteButtons'
- )
- );
- });
-
- test('delete comment button for admins with draft is hidden', () => {
- element._isAdmin = false;
- element.draft = true;
- assert.isFalse(
- queryAndAssert(element, '.action.delete').classList.contains(
- 'showDeleteButtons'
- )
- );
- });
-
- test('delete comment', async () => {
- const stub = stubRestApi('deleteComment').returns(
- Promise.resolve({
- id: '1' as UrlEncodedCommentId,
- updated: '1' as Timestamp,
- ...createComment(),
- })
- );
- const openSpy = sinon.spy(element.confirmDeleteOverlay!, 'open');
- element.changeNum = 42 as NumericChangeId;
- element.patchNum = 1 as PatchSetNum;
- element._isAdmin = true;
- assert.isTrue(
- queryAndAssert(element, '.action.delete').classList.contains(
- 'showDeleteButtons'
- )
- );
- tap(queryAndAssert(element, '.action.delete'));
- await flush();
- await openSpy.lastCall.returnValue;
- const dialog = element.confirmDeleteOverlay?.querySelector(
- '#confirmDeleteComment'
- ) as GrConfirmDeleteCommentDialog;
- dialog.message = 'removal reason';
- element._handleConfirmDeleteComment();
- assert.isTrue(
- stub.calledWith(
- 42 as NumericChangeId,
- 1 as PatchSetNum,
- 'baf0414d_60047215' as UrlEncodedCommentId,
- 'removal reason'
- )
- );
- });
-
- suite('draft update reporting', () => {
- let endStub: SinonStubbedMember<() => Timer>;
- let getTimerStub: sinon.SinonStub;
- const mockEvent = {...new Event('click'), preventDefault() {}};
-
- setup(() => {
- sinon.stub(element, 'save').returns(Promise.resolve({}));
- endStub = sinon.stub();
- const mockTimer = new MockTimer();
- mockTimer.end = endStub;
- getTimerStub = stubReporting('getTimer').returns(mockTimer);
- });
-
- test('create', async () => {
- element.patchNum = 1 as PatchSetNum;
- element.comment = {};
- sinon.stub(element, '_discardDraft').returns(Promise.resolve({}));
- await element._handleSave(mockEvent);
- await flush();
- const grAccountLabel = queryAndAssert(element, 'gr-account-label');
- const spanName = queryAndAssert<HTMLSpanElement>(
- grAccountLabel,
- 'span.name'
- );
- assert.equal(spanName.innerText.trim(), 'Dhruv Srivastava');
- assert.isTrue(endStub.calledOnce);
- assert.isTrue(getTimerStub.calledOnce);
- assert.equal(getTimerStub.lastCall.args[0], 'CreateDraftComment');
- });
-
- test('update', () => {
- element.comment = {
- ...createComment(),
- id: 'abc_123' as UrlEncodedCommentId as UrlEncodedCommentId,
- };
- sinon.stub(element, '_discardDraft').returns(Promise.resolve({}));
- return element._handleSave(mockEvent)!.then(() => {
- assert.isTrue(endStub.calledOnce);
- assert.isTrue(getTimerStub.calledOnce);
- assert.equal(getTimerStub.lastCall.args[0], 'UpdateDraftComment');
- });
- });
-
- test('discard', () => {
- element.comment = {
- ...createComment(),
- id: 'abc_123' as UrlEncodedCommentId as UrlEncodedCommentId,
- };
- element.comment = createDraft();
- sinon.stub(element, '_fireDiscard');
- sinon.stub(element, '_eraseDraftCommentFromStorage');
- sinon
- .stub(element, '_deleteDraft')
- .returns(Promise.resolve(new Response()));
- return element._discardDraft().then(() => {
- assert.isTrue(endStub.calledOnce);
- assert.isTrue(getTimerStub.calledOnce);
- assert.equal(getTimerStub.lastCall.args[0], 'DiscardDraftComment');
- });
- });
- });
-
- test('edit reports interaction', () => {
- const reportStub = stubReporting('recordDraftInteraction');
- sinon.stub(element, '_fireEdit');
- element.draft = true;
- flush();
- tap(queryAndAssert(element, '.edit'));
- assert.isTrue(reportStub.calledOnce);
- });
-
- test('discard reports interaction', () => {
- const reportStub = stubReporting('recordDraftInteraction');
- sinon.stub(element, '_eraseDraftCommentFromStorage');
- sinon.stub(element, '_fireDiscard');
- sinon
- .stub(element, '_deleteDraft')
- .returns(Promise.resolve(new Response()));
- element.draft = true;
- element.comment = createDraft();
- flush();
- tap(queryAndAssert(element, '.discard'));
- assert.isTrue(reportStub.calledOnce);
- });
-
- test('failed save draft request', async () => {
- element.draft = true;
- element.changeNum = 1 as NumericChangeId;
- element.patchNum = 1 as PatchSetNum;
- const updateRequestStub = sinon.stub(element, '_updateRequestToast');
- const diffDraftStub = stubRestApi('saveDiffDraft').returns(
- Promise.resolve({...new Response(), ok: false})
- );
- element._saveDraft({
- ...createComment(),
- id: 'abc_123' as UrlEncodedCommentId,
- });
- await flush();
- let args = updateRequestStub.lastCall.args;
- assert.deepEqual(args, [0, true]);
- assert.equal(
- element._getSavingMessage(...args),
- __testOnly_UNSAVED_MESSAGE
- );
- assert.equal(
- (queryAndAssert(element, '.draftLabel') as HTMLSpanElement).innerText,
- 'DRAFT(Failed to save)'
- );
- assert.isTrue(
- isVisible(queryAndAssert(element, '.save')),
- 'save is visible'
- );
- diffDraftStub.returns(Promise.resolve({...new Response(), ok: true}));
- element._saveDraft({
- ...createComment(),
- id: 'abc_123' as UrlEncodedCommentId,
- });
- await flush();
- args = updateRequestStub.lastCall.args;
- assert.deepEqual(args, [0]);
- assert.equal(element._getSavingMessage(...args), 'All changes saved');
- assert.equal(
- (queryAndAssert(element, '.draftLabel') as HTMLSpanElement).innerText,
- 'DRAFT'
- );
- assert.isFalse(
- isVisible(queryAndAssert(element, '.save')),
- 'save is not visible'
- );
- assert.isFalse(element._unableToSave);
- });
-
- test('failed save draft request with promise failure', async () => {
- element.draft = true;
- element.changeNum = 1 as NumericChangeId;
- element.patchNum = 1 as PatchSetNum;
- const updateRequestStub = sinon.stub(element, '_updateRequestToast');
- const diffDraftStub = stubRestApi('saveDiffDraft').returns(
- Promise.reject(new Error())
- );
- element._saveDraft({
- ...createComment(),
- id: 'abc_123' as UrlEncodedCommentId,
- });
- await flush();
- let args = updateRequestStub.lastCall.args;
- assert.deepEqual(args, [0, true]);
- assert.equal(
- element._getSavingMessage(...args),
- __testOnly_UNSAVED_MESSAGE
- );
- assert.equal(
- (queryAndAssert(element, '.draftLabel') as HTMLSpanElement).innerText,
- 'DRAFT(Failed to save)'
- );
- assert.isTrue(
- isVisible(queryAndAssert(element, '.save')),
- 'save is visible'
- );
- diffDraftStub.returns(Promise.resolve({...new Response(), ok: true}));
- element._saveDraft({
- ...createComment(),
- id: 'abc_123' as UrlEncodedCommentId,
- });
- await flush();
- args = updateRequestStub.lastCall.args;
- assert.deepEqual(args, [0]);
- assert.equal(element._getSavingMessage(...args), 'All changes saved');
- assert.equal(
- (queryAndAssert(element, '.draftLabel') as HTMLSpanElement).innerText,
- 'DRAFT'
- );
- assert.isFalse(
- isVisible(queryAndAssert(element, '.save')),
- 'save is not visible'
- );
- assert.isFalse(element._unableToSave);
+ test('renders draft in editing mode', async () => {
+ element.initiallyCollapsed = false;
+ (element.comment as DraftInfo).__draft = true;
+ element.editing = true;
+ await element.updateComplete;
+ expect(element).shadowDom.to.equal(`
+ <div class="container draft" id="container">
+ <div class="header" id="header">
+ <div class="headerLeft">
+ <gr-account-label class="draft" deselected="" hidestatus=""></gr-account-label>
+ <gr-tooltip-content
+ class="draftTooltip" has-tooltip="" max-width="20em" show-icon=""
+ title="This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'a' key."
+ >
+ <span class="draftLabel">DRAFT</span>
+ </gr-tooltip-content>
+ </div>
+ <div class="headerMiddle"></div>
+ <span class="patchset-text">Patchset 1</span>
+ <span class="separator"></span>
+ <span class="date" tabindex="0">
+ <gr-date-formatter withtooltip=""></gr-date-formatter>
+ </span>
+ <div class="show-hide" tabindex="0">
+ <label aria-label="Collapse" class="show-hide">
+ <input class="show-hide" type="checkbox">
+ <iron-icon id="icon" icon="gr-icons:expand-less"></iron-icon>
+ </label>
+ </div>
+ </div>
+ <div class="body">
+ <gr-textarea
+ autocomplete="on" class="code editMessage" code="" id="editTextarea" rows="4"
+ text="This is the test comment message."
+ >
+ </gr-textarea>
+ <div class="actions">
+ <div class="action resolve">
+ <label>
+ <input checked="" id="resolvedCheckbox" type="checkbox">
+ Resolved
+ </label>
+ </div>
+ <div class="rightActions">
+ <gr-button aria-disabled="false" class="action cancel" link="" role="button" tabindex="0">
+ Cancel
+ </gr-button>
+ <gr-button aria-disabled="false" class="action save" link="" role="button" tabindex="0">
+ Save
+ </gr-button>
+ </div>
+ </div>
+ </div>
+ </div>
+ `);
});
});
- suite('gr-comment draft tests', () => {
- let element: GrComment;
+ test('clicking on date link fires event', async () => {
+ const stub = sinon.stub();
+ element.addEventListener('comment-anchor-tap', stub);
+ await element.updateComplete;
- setup(() => {
- stubRestApi('getAccount').returns(Promise.resolve(undefined));
- stubRestApi('saveDiffDraft').returns(
- Promise.resolve({
- ...new Response(),
- ok: true,
- text() {
- return Promise.resolve(
- ")]}'\n{" +
- '"id": "baf0414d_40572e03",' +
- '"path": "/path/to/file",' +
- '"line": 5,' +
- '"updated": "2015-12-08 21:52:36.177000000",' +
- '"message": "saved!",' +
- '"side": "REVISION",' +
- '"unresolved": false,' +
- '"patch_set": 1' +
- '}'
- );
- },
- })
- );
- stubRestApi('removeChangeReviewer').returns(
- Promise.resolve({...new Response(), ok: true})
- );
- element = draftFixture.instantiate() as GrComment;
- stubStorage('getDraftComment').returns(null);
+ const dateEl = queryAndAssert(element, '.date');
+ tap(dateEl);
+
+ assert.isTrue(stub.called);
+ assert.deepEqual(stub.lastCall.args[0].detail, {
+ side: 'REVISION',
+ number: element.comment!.line,
+ });
+ });
+
+ test('comment message sets messageText only when empty', async () => {
+ element.changeNum = 1 as NumericChangeId;
+ element.messageText = '';
+ element.comment = {
+ ...createComment(),
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com' as EmailAddress,
+ },
+ line: 5,
+ path: 'test',
+ __draft: true,
+ message: 'hello world',
+ };
+ element.editing = true;
+ await element.updateComplete;
+ // messageText was empty so overwrite the message now
+ assert.equal(element.messageText, 'hello world');
+
+ element.comment!.message = 'new message';
+ await element.updateComplete;
+ // messageText was already set so do not overwrite it
+ assert.equal(element.messageText, 'hello world');
+ });
+
+ test('comment message sets messageText when not edited', async () => {
+ element.changeNum = 1 as NumericChangeId;
+ element.messageText = 'Some text';
+ element.comment = {
+ ...createComment(),
+ author: {
+ name: 'Mr. Peanutbutter',
+ email: 'tenn1sballchaser@aol.com' as EmailAddress,
+ },
+ line: 5,
+ path: 'test',
+ __draft: true,
+ message: 'hello world',
+ };
+ element.editing = true;
+ await element.updateComplete;
+ // messageText was empty so overwrite the message now
+ assert.equal(element.messageText, 'hello world');
+
+ element.comment!.message = 'new message';
+ await element.updateComplete;
+ // messageText was already set so do not overwrite it
+ assert.equal(element.messageText, 'hello world');
+ });
+
+ test('delete comment', async () => {
+ element.changeNum = 42 as NumericChangeId;
+ element.isAdmin = true;
+ await element.updateComplete;
+
+ const deleteButton = queryAndAssert(element, '.action.delete');
+ tap(deleteButton);
+ await element.updateComplete;
+
+ assertIsDefined(element.confirmDeleteOverlay, 'confirmDeleteOverlay');
+ const dialog = queryAndAssert<GrConfirmDeleteCommentDialog>(
+ element.confirmDeleteOverlay,
+ '#confirmDeleteComment'
+ );
+ dialog.message = 'removal reason';
+ await element.updateComplete;
+
+ const stub = stubRestApi('deleteComment').returns(
+ Promise.resolve(createComment())
+ );
+ element.handleConfirmDeleteComment();
+ assert.isTrue(
+ stub.calledWith(
+ 42 as NumericChangeId,
+ 1 as PatchSetNum,
+ 'baf0414d_60047215' as UrlEncodedCommentId,
+ 'removal reason'
+ )
+ );
+ });
+
+ suite('gr-comment draft tests', () => {
+ setup(async () => {
element.changeNum = 42 as NumericChangeId;
- element.patchNum = 1 as PatchSetNum;
- element.editing = false;
element.comment = {
...createComment(),
__draft: true,
- __draftID: 'temp_draft_id',
path: '/path/to/file',
line: 5,
- id: undefined,
};
});
- test('button visibility states', async () => {
- element.showActions = false;
- assert.isTrue(
- queryAndAssert(element, '.humanActions').hasAttribute('hidden')
- );
- assert.isTrue(
- queryAndAssert(element, '.robotActions').hasAttribute('hidden')
- );
+ test('isSaveDisabled', async () => {
+ element.saving = false;
+ element.unresolved = true;
+ element.comment = {...createComment(), unresolved: true};
+ element.messageText = 'asdf';
+ await element.updateComplete;
+ assert.isFalse(element.isSaveDisabled());
- element.showActions = true;
- assert.isFalse(
- queryAndAssert(element, '.humanActions').hasAttribute('hidden')
- );
- assert.isTrue(
- queryAndAssert(element, '.robotActions').hasAttribute('hidden')
- );
+ element.messageText = '';
+ await element.updateComplete;
+ assert.isTrue(element.isSaveDisabled());
- element.draft = true;
- await flush();
- assert.isTrue(
- isVisible(queryAndAssert(element, '.edit')),
- 'edit is visible'
- );
- assert.isTrue(
- isVisible(queryAndAssert(element, '.discard')),
- 'discard is visible'
- );
- assert.isFalse(
- isVisible(queryAndAssert(element, '.save')),
- 'save is not visible'
- );
- assert.isFalse(
- isVisible(queryAndAssert(element, '.cancel')),
- 'cancel is not visible'
- );
- assert.isTrue(
- isVisible(queryAndAssert(element, '.resolve')),
- 'resolve is visible'
- );
- assert.isFalse(
- queryAndAssert(element, '.humanActions').hasAttribute('hidden')
- );
- assert.isTrue(
- queryAndAssert(element, '.robotActions').hasAttribute('hidden')
- );
+ element.unresolved = false;
+ await element.updateComplete;
+ assert.isFalse(element.isSaveDisabled());
- element.editing = true;
- await flush();
- assert.isFalse(
- isVisible(queryAndAssert(element, '.edit')),
- 'edit is not visible'
- );
- assert.isFalse(
- isVisible(queryAndAssert(element, '.discard')),
- 'discard not visible'
- );
- assert.isTrue(
- isVisible(queryAndAssert(element, '.save')),
- 'save is visible'
- );
- assert.isTrue(
- isVisible(queryAndAssert(element, '.cancel')),
- 'cancel is visible'
- );
- assert.isTrue(
- isVisible(queryAndAssert(element, '.resolve')),
- 'resolve is visible'
- );
- assert.isFalse(
- queryAndAssert(element, '.humanActions').hasAttribute('hidden')
- );
- assert.isTrue(
- queryAndAssert(element, '.robotActions').hasAttribute('hidden')
- );
-
- element.draft = false;
- element.editing = false;
- await flush();
- assert.isFalse(
- isVisible(queryAndAssert(element, '.edit')),
- 'edit is not visible'
- );
- assert.isFalse(
- isVisible(queryAndAssert(element, '.discard')),
- 'discard is not visible'
- );
- assert.isFalse(
- isVisible(queryAndAssert(element, '.save')),
- 'save is not visible'
- );
- assert.isFalse(
- isVisible(queryAndAssert(element, '.cancel')),
- 'cancel is not visible'
- );
- assert.isFalse(
- queryAndAssert(element, '.humanActions').hasAttribute('hidden')
- );
- assert.isTrue(
- queryAndAssert(element, '.robotActions').hasAttribute('hidden')
- );
-
- element.comment!.id = 'foo' as UrlEncodedCommentId;
- element.draft = true;
- element.editing = true;
- await flush();
- assert.isTrue(
- isVisible(queryAndAssert(element, '.cancel')),
- 'cancel is visible'
- );
- assert.isFalse(
- queryAndAssert(element, '.humanActions').hasAttribute('hidden')
- );
- assert.isTrue(
- queryAndAssert(element, '.robotActions').hasAttribute('hidden')
- );
-
- // Delete button is not hidden by default
- assert.isFalse(
- (queryAndAssert(element, '#deleteBtn') as HTMLElement).hidden
- );
-
- element.isRobotComment = true;
- element.draft = true;
- assert.isTrue(
- queryAndAssert(element, '.humanActions').hasAttribute('hidden')
- );
- assert.isFalse(
- queryAndAssert(element, '.robotActions').hasAttribute('hidden')
- );
-
- // It is not expected to see Robot comment drafts, but if they appear,
- // they will behave the same as non-drafts.
- element.draft = false;
- assert.isTrue(
- queryAndAssert(element, '.humanActions').hasAttribute('hidden')
- );
- assert.isFalse(
- queryAndAssert(element, '.robotActions').hasAttribute('hidden')
- );
-
- // A robot comment with run ID should display plain text.
- element.set(['comment', 'robot_run_id'], 'text');
- element.editing = false;
- element.collapsed = false;
- await flush();
- assert.isTrue(
- queryAndAssert(element, '.robotRun.link').textContent === 'Run Details'
- );
-
- // A robot comment with run ID and url should display a link.
- element.set(['comment', 'url'], '/path/to/run');
- await flush();
- assert.notEqual(
- getComputedStyle(queryAndAssert(element, '.robotRun.link')).display,
- 'none'
- );
-
- // Delete button is hidden for robot comments
- assert.isTrue(
- (queryAndAssert(element, '#deleteBtn') as HTMLElement).hidden
- );
- });
-
- test('collapsible drafts', async () => {
- const fireEditStub = sinon.stub(element, '_fireEdit');
- assert.isTrue(element.collapsed);
- assert.isFalse(
- isVisible(queryAndAssert(element, 'gr-formatted-text')),
- 'gr-formatted-text is not visible'
- );
- assert.isFalse(
- isVisible(queryAndAssert(element, '.actions')),
- 'actions are not visible'
- );
- assert.isNotOk(element.textarea, 'textarea is not visible');
- assert.isTrue(
- isVisible(queryAndAssert(element, '.collapsedContent')),
- 'header middle content is visible'
- );
-
- tap(element.$.header);
- assert.isFalse(element.collapsed);
- assert.isTrue(
- isVisible(queryAndAssert(element, 'gr-formatted-text')),
- 'gr-formatted-text is visible'
- );
- assert.isTrue(
- isVisible(queryAndAssert(element, '.actions')),
- 'actions are visible'
- );
- assert.isNotOk(element.textarea, 'textarea is not visible');
- assert.isFalse(
- isVisible(queryAndAssert(element, '.collapsedContent')),
- 'header middle content is is not visible'
- );
-
- // When the edit button is pressed, should still see the actions
- // and also textarea
- element.draft = true;
- await flush();
- tap(queryAndAssert(element, '.edit'));
- await flush();
- assert.isTrue(fireEditStub.called);
- assert.isFalse(element.collapsed);
- assert.isFalse(
- isVisible(queryAndAssert(element, 'gr-formatted-text')),
- 'gr-formatted-text is not visible'
- );
- assert.isTrue(
- isVisible(queryAndAssert(element, '.actions')),
- 'actions are visible'
- );
- assert.isTrue(isVisible(element.textarea!), 'textarea is visible');
- assert.isFalse(
- isVisible(queryAndAssert(element, '.collapsedContent')),
- 'header middle content is not visible'
- );
-
- // When toggle again, everything should be hidden except for textarea
- // and header middle content should be visible
- tap(element.$.header);
- assert.isTrue(element.collapsed);
- assert.isFalse(
- isVisible(queryAndAssert(element, 'gr-formatted-text')),
- 'gr-formatted-text is not visible'
- );
- assert.isFalse(
- isVisible(queryAndAssert(element, '.actions')),
- 'actions are not visible'
- );
- assert.isFalse(
- isVisible(queryAndAssert(element, 'gr-textarea')),
- 'textarea is not visible'
- );
- assert.isTrue(
- isVisible(queryAndAssert(element, '.collapsedContent')),
- 'header middle content is visible'
- );
-
- // When toggle again, textarea should remain open in the state it was
- // before
- tap(element.$.header);
- assert.isFalse(
- isVisible(queryAndAssert(element, 'gr-formatted-text')),
- 'gr-formatted-text is not visible'
- );
- assert.isTrue(
- isVisible(queryAndAssert(element, '.actions')),
- 'actions are visible'
- );
- assert.isTrue(isVisible(element.textarea!), 'textarea is visible');
- assert.isFalse(
- isVisible(queryAndAssert(element, '.collapsedContent')),
- 'header middle content is not visible'
- );
- });
-
- test('robot comment layout', async () => {
- const comment = {
- robot_id: 'happy_robot_id' as RobotId,
- url: '/robot/comment',
- author: {
- name: 'Happy Robot',
- display_name: 'Display name Robot',
- },
- ...element.comment,
- };
- element.comment = comment;
- element.collapsed = false;
- await flush;
- let runIdMessage;
- runIdMessage = queryAndAssert(element, '.runIdMessage') as HTMLElement;
- assert.isFalse((runIdMessage as HTMLElement).hidden);
-
- const runDetailsLink = queryAndAssert(
- element,
- '.robotRunLink'
- ) as HTMLAnchorElement;
- assert.isTrue(
- runDetailsLink.href.indexOf((element.comment as UIRobot).url!) !== -1
- );
-
- const robotServiceName = queryAndAssert(element, '.robotName');
- assert.equal(robotServiceName.textContent?.trim(), 'happy_robot_id');
-
- const authorName = queryAndAssert(element, '.robotId');
- assert.isTrue((authorName as HTMLDivElement).innerText === 'Happy Robot');
-
- element.collapsed = true;
- await flush();
- runIdMessage = queryAndAssert(element, '.runIdMessage');
- assert.isTrue((runIdMessage as HTMLDivElement).hidden);
- });
-
- test('author name fallback to email', async () => {
- const comment = {
- url: '/robot/comment',
- author: {
- email: 'test@test.com' as EmailAddress,
- },
- ...element.comment,
- };
- element.comment = comment;
- element.collapsed = false;
- await flush();
- const authorName = queryAndAssert(
- queryAndAssert(element, 'gr-account-label'),
- 'span.name'
- ) as HTMLSpanElement;
- assert.equal(authorName.innerText.trim(), 'test@test.com');
- });
-
- test('patchset level comment', async () => {
- const fireEditStub = sinon.stub(element, '_fireEdit');
- const comment = {
- ...element.comment,
- path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
- line: undefined,
- range: undefined,
- };
- element.comment = comment;
- await flush();
- tap(queryAndAssert(element, '.edit'));
- assert.isTrue(fireEditStub.called);
- assert.isTrue(element.editing);
-
- element._messageText = 'hello world';
- const eraseMessageDraftSpy = spyStorage('eraseDraftComment');
- const mockEvent = {...new Event('click'), preventDefault: sinon.stub()};
- element._handleSave(mockEvent);
- await flush();
- assert.isTrue(eraseMessageDraftSpy.called);
- });
-
- test('draft creation/cancellation', async () => {
- const fireEditStub = sinon.stub(element, '_fireEdit');
- assert.isFalse(element.editing);
- element.draft = true;
- await flush();
- tap(queryAndAssert(element, '.edit'));
- assert.isTrue(fireEditStub.called);
- assert.isTrue(element.editing);
-
- element.comment!.message = '';
- element._messageText = '';
- const eraseMessageDraftSpy = sinon.spy(
- element,
- '_eraseDraftCommentFromStorage'
- );
-
- // Save should be disabled on an empty message.
- let disabled = queryAndAssert(element, '.save').hasAttribute('disabled');
- assert.isTrue(disabled, 'save button should be disabled.');
- element._messageText = ' ';
- disabled = queryAndAssert(element, '.save').hasAttribute('disabled');
- assert.isTrue(disabled, 'save button should be disabled.');
-
- const updateStub = sinon.stub();
- element.addEventListener('comment-update', updateStub);
-
- let numDiscardEvents = 0;
- const promise = mockPromise();
- element.addEventListener('comment-discard', () => {
- numDiscardEvents++;
- assert.isFalse(eraseMessageDraftSpy.called);
- if (numDiscardEvents === 2) {
- assert.isFalse(updateStub.called);
- promise.resolve();
- }
- });
- tap(queryAndAssert(element, '.cancel'));
- await flush();
- element._messageText = '';
- element.editing = true;
- await flush();
- pressAndReleaseKeyOn(element.textarea!, 27, null, 'Escape');
- await promise;
- });
-
- test('draft discard removes message from storage', async () => {
- element._messageText = '';
- const eraseMessageDraftSpy = sinon.spy(
- element,
- '_eraseDraftCommentFromStorage'
- );
-
- const promise = mockPromise();
- element.addEventListener('comment-discard', () => {
- assert.isTrue(eraseMessageDraftSpy.called);
- promise.resolve();
- });
- element._handleDiscard({
- ...new Event('click'),
- preventDefault: sinon.stub(),
- });
- await promise;
- });
-
- test('storage is cleared only after save success', () => {
- element._messageText = 'test';
- const eraseStub = sinon.stub(element, '_eraseDraftCommentFromStorage');
- stubRestApi('getResponseObject').returns(
- Promise.resolve({...(createDraft() as ParsedJSON)})
- );
- const saveDraftStub = sinon
- .stub(element, '_saveDraft')
- .returns(Promise.resolve({...new Response(), ok: false}));
-
- const savePromise = element.save();
- assert.isFalse(eraseStub.called);
- return savePromise.then(() => {
- assert.isFalse(eraseStub.called);
-
- saveDraftStub.restore();
- sinon
- .stub(element, '_saveDraft')
- .returns(Promise.resolve({...new Response(), ok: true}));
- return element.save().then(() => {
- assert.isTrue(eraseStub.called);
- });
- });
- });
-
- test('_computeSaveDisabled', () => {
- const comment = {unresolved: true};
- const msgComment = {message: 'test', unresolved: true};
- assert.equal(element._computeSaveDisabled('', comment, false), true);
- assert.equal(element._computeSaveDisabled('test', comment, false), false);
- assert.equal(element._computeSaveDisabled('', msgComment, false), true);
- assert.equal(
- element._computeSaveDisabled('test', msgComment, false),
- false
- );
- assert.equal(
- element._computeSaveDisabled('test2', msgComment, false),
- false
- );
- assert.equal(element._computeSaveDisabled('test', comment, true), false);
- assert.equal(element._computeSaveDisabled('', comment, true), true);
- assert.equal(element._computeSaveDisabled('', comment, false), true);
+ element.saving = true;
+ await element.updateComplete;
+ assert.isTrue(element.isSaveDisabled());
});
test('ctrl+s saves comment', async () => {
- const promise = mockPromise();
- const stub = sinon.stub(element, 'save').callsFake(() => {
- assert.isTrue(stub.called);
- stub.restore();
- promise.resolve();
- return Promise.resolve();
- });
- element._messageText = 'is that the horse from horsing around??';
+ const spy = sinon.stub(element, 'save');
+ element.messageText = 'is that the horse from horsing around??';
element.editing = true;
- await flush();
- pressAndReleaseKeyOn(
- element.textarea!.$.textarea.textarea,
- 83,
- 'ctrl',
- 's'
- );
- await promise;
+ await element.updateComplete;
+ pressKey(element.textarea!.$.textarea.textarea, 's', Modifier.CTRL_KEY);
+ assert.isTrue(spy.called);
});
- test('draft saving/editing', async () => {
- const dispatchEventStub = sinon.stub(element, 'dispatchEvent');
- const fireEditStub = sinon.stub(element, '_fireEdit');
- const clock: SinonFakeTimers = sinon.useFakeTimers();
- const tickAndFlush = async (repetitions: number) => {
- for (let i = 1; i <= repetitions; i++) {
- clock.tick(1000);
- await flush();
- }
- };
+ test('save', async () => {
+ const savePromise = mockPromise<DraftInfo>();
+ const stub = sinon
+ .stub(element.getCommentsModel(), 'saveDraft')
+ .returns(savePromise);
- element.draft = true;
- await flush();
- tap(queryAndAssert(element, '.edit'));
- assert.isTrue(fireEditStub.called);
- tickAndFlush(1);
- element._messageText = 'good news, everyone!';
- tickAndFlush(1);
- assert.equal(dispatchEventStub.lastCall.args[0].type, 'comment-update');
- assert.isTrue(dispatchEventStub.calledTwice);
-
- element._messageText = 'good news, everyone!';
- await flush();
- assert.isTrue(dispatchEventStub.calledTwice);
-
- tap(queryAndAssert(element, '.save'));
-
- assert.isTrue(
- element.disabled,
- 'Element should be disabled when creating draft.'
- );
-
- let draft = await element._xhrPromise!;
- const evt = dispatchEventStub.lastCall.args[0] as CustomEvent<{
- comment: DraftInfo;
- }>;
- assert.equal(evt.type, 'comment-save');
-
- const expectedDetail = {
- comment: {
- ...createComment(),
- __draft: true,
- __draftID: 'temp_draft_id',
- id: 'baf0414d_40572e03' as UrlEncodedCommentId,
- line: 5,
- message: 'saved!',
- path: '/path/to/file',
- updated: '2015-12-08 21:52:36.177000000' as Timestamp,
- },
- patchNum: 1 as PatchSetNum,
- };
-
- assert.deepEqual(evt.detail, expectedDetail);
- assert.isFalse(
- element.disabled,
- 'Element should be enabled when done creating draft.'
- );
- assert.equal(draft.message, 'saved!');
- assert.isFalse(element.editing);
- tap(queryAndAssert(element, '.edit'));
- assert.isTrue(fireEditStub.calledTwice);
- element._messageText =
- 'You’ll be delivering a package to Chapek 9, ' +
- 'a world where humans are killed on sight.';
- tap(queryAndAssert(element, '.save'));
- assert.isTrue(
- element.disabled,
- 'Element should be disabled when updating draft.'
- );
- draft = await element._xhrPromise!;
- assert.isFalse(
- element.disabled,
- 'Element should be enabled when done updating draft.'
- );
- assert.equal(draft.message, 'saved!');
- assert.isFalse(element.editing);
- dispatchEventStub.restore();
- });
-
- test('draft prevent save when disabled', async () => {
- const saveStub = sinon.stub(element, 'save').returns(Promise.resolve());
- element.showActions = true;
- element.draft = true;
- await flush();
- tap(element.$.header);
- tap(queryAndAssert(element, '.edit'));
- element._messageText = 'good news, everyone!';
- await flush();
-
- element.disabled = true;
- tap(queryAndAssert(element, '.save'));
- assert.isFalse(saveStub.called);
-
- element.disabled = false;
- tap(queryAndAssert(element, '.save'));
- assert.isTrue(saveStub.calledOnce);
- });
-
- test('proper event fires on resolve, comment is not saved', async () => {
- const save = sinon.stub(element, 'save');
- const promise = mockPromise();
- element.addEventListener('comment-update', e => {
- assert.isTrue(e.detail.comment.unresolved);
- assert.isFalse(save.called);
- promise.resolve();
- });
- tap(queryAndAssert(element, '.resolve input'));
- await promise;
- });
-
- test('resolved comment state indicated by checkbox', () => {
- sinon.stub(element, 'save');
- element.comment = {unresolved: false};
- assert.isTrue(
- (queryAndAssert(element, '.resolve input') as HTMLInputElement).checked
- );
- element.comment = {unresolved: true};
- assert.isFalse(
- (queryAndAssert(element, '.resolve input') as HTMLInputElement).checked
- );
- });
-
- test('resolved checkbox saves with tap when !editing', () => {
- element.editing = false;
- const save = sinon.stub(element, 'save');
-
- element.comment = {unresolved: false};
- assert.isTrue(
- (queryAndAssert(element, '.resolve input') as HTMLInputElement).checked
- );
- element.comment = {unresolved: true};
- assert.isFalse(
- (queryAndAssert(element, '.resolve input') as HTMLInputElement).checked
- );
- assert.isFalse(save.called);
- tap(element.$.resolvedCheckbox);
- assert.isTrue(
- (queryAndAssert(element, '.resolve input') as HTMLInputElement).checked
- );
- assert.isTrue(save.called);
- });
-
- suite('draft saving messages', () => {
- test('_getSavingMessage', () => {
- assert.equal(element._getSavingMessage(0), 'All changes saved');
- assert.equal(element._getSavingMessage(1), 'Saving 1 draft...');
- assert.equal(element._getSavingMessage(2), 'Saving 2 drafts...');
- assert.equal(element._getSavingMessage(3), 'Saving 3 drafts...');
- });
-
- test('_show{Start,End}Request', () => {
- const updateStub = sinon.stub(element, '_updateRequestToast');
- element._numPendingDraftRequests.number = 1;
-
- element._showStartRequest();
- assert.isTrue(updateStub.calledOnce);
- assert.equal(updateStub.lastCall.args[0], 2);
- assert.equal(element._numPendingDraftRequests.number, 2);
-
- element._showEndRequest();
- assert.isTrue(updateStub.calledTwice);
- assert.equal(updateStub.lastCall.args[0], 1);
- assert.equal(element._numPendingDraftRequests.number, 1);
-
- element._showEndRequest();
- assert.isTrue(updateStub.calledThrice);
- assert.equal(updateStub.lastCall.args[0], 0);
- assert.equal(element._numPendingDraftRequests.number, 0);
- });
- });
-
- test('cancelling an unsaved draft discards, persists in storage', async () => {
- const clock: SinonFakeTimers = sinon.useFakeTimers();
- const tickAndFlush = async (repetitions: number) => {
- for (let i = 1; i <= repetitions; i++) {
- clock.tick(1000);
- await flush();
- }
- };
- const discardSpy = sinon.spy(element, '_fireDiscard');
- const storeStub = stubStorage('setDraftComment');
- const eraseStub = stubStorage('eraseDraftComment');
- element.comment!.id = undefined; // set id undefined for draft
- element._messageText = 'test text';
- tickAndFlush(1);
-
- assert.isTrue(storeStub.called);
- assert.equal(storeStub.lastCall.args[1], 'test text');
- element._handleCancel({
- ...new Event('click'),
- preventDefault: sinon.stub(),
- });
- await flush();
- assert.isTrue(discardSpy.called);
- assert.isFalse(eraseStub.called);
- });
-
- test('cancelling edit on a saved draft does not store', () => {
- element.comment!.id = 'foo' as UrlEncodedCommentId;
- const discardSpy = sinon.spy(element, '_fireDiscard');
- const storeStub = stubStorage('setDraftComment');
- element.comment!.id = undefined; // set id undefined for draft
- element._messageText = 'test text';
- flush();
-
- assert.isFalse(storeStub.called);
- element._handleCancel({...new Event('click'), preventDefault: () => {}});
- assert.isTrue(discardSpy.called);
- });
-
- test('deleting text from saved draft and saving deletes the draft', () => {
- element.comment = {
- ...createComment(),
- id: 'foo' as UrlEncodedCommentId,
- message: 'test',
- };
- element._messageText = '';
- const discardStub = sinon.stub(element, '_discardDraft');
+ element.comment = createDraft();
+ element.editing = true;
+ await element.updateComplete;
+ const textToSave = 'something, not important';
+ element.messageText = textToSave;
+ element.unresolved = true;
+ await element.updateComplete;
element.save();
- assert.isTrue(discardStub.called);
+
+ await element.updateComplete;
+ waitUntilCalled(stub, 'saveDraft()');
+ assert.equal(stub.lastCall.firstArg.message, textToSave);
+ assert.equal(stub.lastCall.firstArg.unresolved, true);
+ assert.isTrue(element.editing);
+ assert.isTrue(element.saving);
+
+ savePromise.resolve();
+ await element.updateComplete;
+
+ assert.isFalse(element.editing);
+ assert.isFalse(element.saving);
});
- test('_handleFix fires create-fix event', async () => {
- const promise = mockPromise();
- element.addEventListener(
- 'create-fix-comment',
- (e: CreateFixCommentEvent) => {
- assert.deepEqual(e.detail, element._getEventPayload());
- promise.resolve();
- }
- );
- element.isRobotComment = true;
- element.comments = [element.comment!];
- await flush();
+ test('save failed', async () => {
+ sinon
+ .stub(element.getCommentsModel(), 'saveDraft')
+ .returns(Promise.reject(new Error('saving failed')));
- tap(queryAndAssert(element, '.fix'));
- await promise;
+ element.comment = createDraft();
+ element.editing = true;
+ await element.updateComplete;
+ element.messageText = 'something, not important';
+ await element.updateComplete;
+
+ element.save();
+ await element.updateComplete;
+
+ assert.isTrue(element.unableToSave);
+ assert.isTrue(element.editing);
+ assert.isFalse(element.saving);
});
- test('do not show Please Fix button if human reply exists', () => {
- element.comments = [
- {
- robot_id: 'happy_robot_id' as RobotId,
- robot_run_id: '5838406743490560' as RobotRunId,
- fix_suggestions: [
- {
- fix_id: '478ff847_3bf47aaf' as FixId,
- description: 'Make the smiley happier by giving it a nose.',
- replacements: [
- {
- path: 'Documentation/config-gerrit.txt',
- range: {
- start_line: 10,
- start_character: 7,
- end_line: 10,
- end_character: 9,
- },
- replacement: ':-)',
- },
- ],
- },
- ],
- author: {
- _account_id: 1030912 as AccountId,
- name: 'Alice Kober-Sotzek',
- email: 'aliceks@google.com' as EmailAddress,
- avatars: [
- {
- url: '/s32-p/photo.jpg',
- height: 32,
- width: 32,
- },
- {
- url: '/AaAdOFzPlFI/s56-p/photo.jpg',
- height: 56,
- width: 32,
- },
- {
- url: '/AaAdOFzPlFI/s100-p/photo.jpg',
- height: 100,
- width: 32,
- },
- {
- url: '/AaAdOFzPlFI/s120-p/photo.jpg',
- height: 120,
- width: 32,
- },
- ],
- },
- patch_set: 1 as PatchSetNum,
- ...createComment(),
- id: 'eb0d03fd_5e95904f' as UrlEncodedCommentId,
- line: 10,
- updated: '2017-04-04 15:36:17.000000000' as Timestamp,
- message: 'This is a robot comment with a fix.',
- unresolved: false,
- collapsed: false,
- },
- {
- __draft: true,
- __draftID: '0.wbrfbwj89sa',
- __date: new Date(),
- path: 'Documentation/config-gerrit.txt',
- side: CommentSide.REVISION,
- line: 10,
- in_reply_to: 'eb0d03fd_5e95904f' as UrlEncodedCommentId,
- message: '> This is a robot comment with a fix.\n\nPlease fix.',
- unresolved: true,
- },
- ];
- element.comment = element.comments[0];
- flush();
- assert.isNull(
- element.shadowRoot?.querySelector('robotActions gr-button')
- );
+ test('discard', async () => {
+ const discardPromise = mockPromise<void>();
+ const stub = sinon
+ .stub(element.getCommentsModel(), 'discardDraft')
+ .returns(discardPromise);
+
+ element.comment = createDraft();
+ element.editing = true;
+ await element.updateComplete;
+
+ element.discard();
+
+ await element.updateComplete;
+ waitUntilCalled(stub, 'discardDraft()');
+ assert.equal(stub.lastCall.firstArg, element.comment.id);
+ assert.isTrue(element.editing);
+ assert.isTrue(element.saving);
+
+ discardPromise.resolve();
+ await element.updateComplete;
+
+ assert.isFalse(element.editing);
+ assert.isFalse(element.saving);
});
- test('show Please Fix if no human reply', () => {
- element.comments = [
- {
- robot_id: 'happy_robot_id' as RobotId,
- robot_run_id: '5838406743490560' as RobotRunId,
- fix_suggestions: [
- {
- fix_id: '478ff847_3bf47aaf' as FixId,
- description: 'Make the smiley happier by giving it a nose.',
- replacements: [
- {
- path: 'Documentation/config-gerrit.txt',
- range: {
- start_line: 10,
- start_character: 7,
- end_line: 10,
- end_character: 9,
- },
- replacement: ':-)',
- },
- ],
- },
- ],
- author: {
- _account_id: 1030912 as AccountId,
- name: 'Alice Kober-Sotzek',
- email: 'aliceks@google.com' as EmailAddress,
- avatars: [
- {
- url: '/s32-p/photo.jpg',
- height: 32,
- width: 32,
- },
- {
- url: '/AaAdOFzPlFI/s56-p/photo.jpg',
- height: 56,
- width: 32,
- },
- {
- url: '/AaAdOFzPlFI/s100-p/photo.jpg',
- height: 100,
- width: 32,
- },
- {
- url: '/AaAdOFzPlFI/s120-p/photo.jpg',
- height: 120,
- width: 32,
- },
- ],
- },
- patch_set: 1 as PatchSetNum,
- ...createComment(),
- id: 'eb0d03fd_5e95904f' as UrlEncodedCommentId,
- line: 10,
- updated: '2017-04-04 15:36:17.000000000' as Timestamp,
- message: 'This is a robot comment with a fix.',
- unresolved: false,
- collapsed: false,
- },
- ];
- element.comment = element.comments[0];
- flush();
- queryAndAssert(element, '.robotActions gr-button');
- });
-
- test('_handleShowFix fires open-fix-preview event', async () => {
- const promise = mockPromise();
- element.addEventListener('open-fix-preview', e => {
- assert.deepEqual(e.detail, element._getEventPayload());
- promise.resolve();
- });
+ test('resolved comment state indicated by checkbox', async () => {
+ const saveStub = sinon.stub(element.getCommentsModel(), 'saveDraft');
element.comment = {
...createComment(),
+ __draft: true,
+ unresolved: false,
+ };
+ await element.updateComplete;
+
+ let checkbox = queryAndAssert<HTMLInputElement>(
+ element,
+ '#resolvedCheckbox'
+ );
+ assert.isTrue(checkbox.checked);
+
+ tap(checkbox);
+ await element.updateComplete;
+
+ checkbox = queryAndAssert<HTMLInputElement>(element, '#resolvedCheckbox');
+ assert.isFalse(checkbox.checked);
+
+ assert.isTrue(saveStub.called);
+ });
+
+ test('saving empty text calls discard()', async () => {
+ const saveStub = sinon.stub(element.getCommentsModel(), 'saveDraft');
+ const discardStub = sinon.stub(
+ element.getCommentsModel(),
+ 'discardDraft'
+ );
+ element.comment = createDraft();
+ element.editing = true;
+ await element.updateComplete;
+
+ element.messageText = '';
+ await element.updateComplete;
+
+ await element.save();
+ assert.isTrue(discardStub.called);
+ assert.isFalse(saveStub.called);
+ });
+
+ test('handleFix fires create-fix event', async () => {
+ const listener = listenOnce<CreateFixCommentEvent>(
+ element,
+ 'create-fix-comment'
+ );
+ element.comment = createRobotComment();
+ element.comments = [element.comment!];
+ await element.updateComplete;
+
+ tap(queryAndAssert(element, '.fix'));
+
+ const e = await listener;
+ assert.deepEqual(e.detail, element.getEventPayload());
+ });
+
+ test('do not show Please Fix button if human reply exists', async () => {
+ element.initiallyCollapsed = false;
+ const robotComment = createRobotComment();
+ element.comment = robotComment;
+ await element.updateComplete;
+
+ let actions = query(element, '.robotActions gr-button.fix');
+ assert.isOk(actions);
+
+ element.comments = [
+ robotComment,
+ {...createComment(), in_reply_to: robotComment.id},
+ ];
+ await element.updateComplete;
+ actions = query(element, '.robotActions gr-button.fix');
+ assert.isNotOk(actions);
+ });
+
+ test('handleShowFix fires open-fix-preview event', async () => {
+ const listener = listenOnce<CustomEvent<OpenFixPreviewEventDetail>>(
+ element,
+ 'open-fix-preview'
+ );
+ element.comment = {
+ ...createRobotComment(),
fix_suggestions: [{...createFixSuggestionInfo()}],
};
- element.isRobotComment = true;
- await flush();
+ await element.updateComplete;
tap(queryAndAssert(element, '.show-fix'));
- await promise;
+
+ const e = await listener;
+ assert.deepEqual(e.detail, element.getEventPayload());
+ });
+ });
+
+ suite('auto saving', () => {
+ let clock: sinon.SinonFakeTimers;
+ let savePromise: MockPromise<DraftInfo>;
+ let saveStub: SinonStub;
+
+ setup(async () => {
+ clock = sinon.useFakeTimers();
+ savePromise = mockPromise<DraftInfo>();
+ saveStub = sinon
+ .stub(element.getCommentsModel(), 'saveDraft')
+ .returns(savePromise);
+
+ element.comment = createUnsaved();
+ element.editing = true;
+ await element.updateComplete;
+ });
+
+ teardown(() => {
+ clock.restore();
+ sinon.restore();
+ });
+
+ test('basic auto saving', async () => {
+ const textarea = queryAndAssert<HTMLElement>(element, '#editTextarea');
+ dispatch(textarea, 'text-changed', {value: 'some new text '});
+
+ clock.tick(AUTO_SAVE_DEBOUNCE_DELAY_MS / 2);
+ assert.isFalse(saveStub.called);
+
+ clock.tick(AUTO_SAVE_DEBOUNCE_DELAY_MS);
+ assert.isTrue(saveStub.called);
+ assert.equal(
+ saveStub.firstCall.firstArg.message,
+ 'some new text '.trimEnd()
+ );
+ });
+
+ test('saving while auto saving', async () => {
+ const textarea = queryAndAssert<HTMLElement>(element, '#editTextarea');
+ dispatch(textarea, 'text-changed', {value: 'auto save text'});
+
+ clock.tick(2 * AUTO_SAVE_DEBOUNCE_DELAY_MS);
+ assert.isTrue(saveStub.called);
+ assert.equal(saveStub.firstCall.firstArg.message, 'auto save text');
+ saveStub.reset();
+
+ element.messageText = 'actual save text';
+ const save = element.save();
+ await element.updateComplete;
+ // First wait for the auto saving to finish.
+ assert.isFalse(saveStub.called);
+
+ // Resolve auto-saving promise.
+ savePromise.resolve({
+ ...element.comment,
+ __draft: true,
+ id: 'exp123' as UrlEncodedCommentId,
+ updated: '2018-02-13 22:48:48.018000000' as Timestamp,
+ });
+ await save;
+ // Only then save.
+ assert.isTrue(saveStub.called);
+ assert.equal(saveStub.firstCall.firstArg.message, 'actual save text');
+ assert.equal(saveStub.firstCall.firstArg.id, 'exp123');
});
});
suite('respectful tips', () => {
- let element: GrComment;
-
let clock: sinon.SinonFakeTimers;
- setup(() => {
- stubRestApi('getAccount').returns(Promise.resolve(undefined));
+ setup(async () => {
clock = sinon.useFakeTimers();
});
@@ -1518,81 +696,96 @@
});
test('show tip when no cached record', async () => {
- element = draftFixture.instantiate() as GrComment;
const respectfulGetStub = stubStorage('getRespectfulTipVisibility');
const respectfulSetStub = stubStorage('setRespectfulTipVisibility');
respectfulGetStub.returns(null);
- // fake random
- element.getRandomNum = () => 0;
- element.comment = {__editing: true, __draft: true};
- await flush();
+ element.editing = true;
+ element.getRandomInt = () => 0;
+ element.comment = createDraft();
+ await element.updateComplete;
+
assert.isTrue(respectfulGetStub.called);
assert.isTrue(respectfulSetStub.called);
- assert.isTrue(!!queryAndAssert(element, '.respectfulReviewTip'));
+ queryAndAssert(element, '.respectfulReviewTip');
});
test('add 14-day delays once dismissed', async () => {
- element = draftFixture.instantiate() as GrComment;
const respectfulGetStub = stubStorage('getRespectfulTipVisibility');
const respectfulSetStub = stubStorage('setRespectfulTipVisibility');
respectfulGetStub.returns(null);
- // fake random
- element.getRandomNum = () => 0;
- element.comment = {__editing: true, __draft: true};
- await flush();
+ element.editing = true;
+ element.getRandomInt = () => 0;
+ element.comment = createDraft();
+ await element.updateComplete;
+
assert.isTrue(respectfulGetStub.called);
assert.isTrue(respectfulSetStub.called);
assert.isTrue(respectfulSetStub.lastCall.args[0] === undefined);
- assert.isTrue(!!queryAndAssert(element, '.respectfulReviewTip'));
+ const closeLink = queryAndAssert(element, '.respectfulReviewTip a.close');
+ tap(closeLink);
+ await element.updateComplete;
- tap(queryAndAssert(element, '.respectfulReviewTip .close'));
- flush();
assert.isTrue(respectfulSetStub.lastCall.args[0] === 14);
});
test('do not show tip when fall out of probability', async () => {
- element = draftFixture.instantiate() as GrComment;
const respectfulGetStub = stubStorage('getRespectfulTipVisibility');
const respectfulSetStub = stubStorage('setRespectfulTipVisibility');
respectfulGetStub.returns(null);
- // fake random
- element.getRandomNum = () => 3;
- element.comment = {__editing: true, __draft: true};
- await flush();
+ element.editing = true;
+ element.getRandomInt = () => 2;
+ element.comment = createDraft();
+ await element.updateComplete;
+
assert.isTrue(respectfulGetStub.called);
assert.isFalse(respectfulSetStub.called);
assert.isNotOk(query(element, '.respectfulReviewTip'));
});
test('show tip when editing changed to true', async () => {
- element = draftFixture.instantiate() as GrComment;
const respectfulGetStub = stubStorage('getRespectfulTipVisibility');
const respectfulSetStub = stubStorage('setRespectfulTipVisibility');
respectfulGetStub.returns(null);
- // fake random
- element.getRandomNum = () => 0;
- element.comment = {__editing: false};
- await flush();
+ element.editing = false;
+ element.getRandomInt = () => 0;
+ element.comment = createComment();
+ await element.updateComplete;
+
assert.isFalse(respectfulGetStub.called);
assert.isFalse(respectfulSetStub.called);
assert.isNotOk(query(element, '.respectfulReviewTip'));
element.editing = true;
- await flush();
+ await element.updateComplete;
assert.isTrue(respectfulGetStub.called);
assert.isTrue(respectfulSetStub.called);
assert.isTrue(!!queryAndAssert(element, '.respectfulReviewTip'));
});
+ test('hide tip when leaving editing mode', async () => {
+ stubStorage('getRespectfulTipVisibility').returns(null);
+ element.editing = true;
+ element.getRandomInt = () => 0;
+ element.comment = createComment();
+
+ await element.updateComplete;
+ assert.isOk(query(element, '.respectfulReviewTip'));
+
+ element.editing = false;
+
+ await element.updateComplete;
+ assert.isNotOk(query(element, '.respectfulReviewTip'));
+ });
+
test('no tip when cached record', async () => {
- element = draftFixture.instantiate() as GrComment;
const respectfulGetStub = stubStorage('getRespectfulTipVisibility');
const respectfulSetStub = stubStorage('setRespectfulTipVisibility');
respectfulGetStub.returns({updated: 0});
- // fake random
- element.getRandomNum = () => 0;
- element.comment = {__editing: true, __draft: true};
- await flush();
+ element.editing = true;
+ element.getRandomInt = () => 0;
+ element.comment = createDraft();
+ await element.updateComplete;
+
assert.isTrue(respectfulGetStub.called);
assert.isFalse(respectfulSetStub.called);
assert.isNotOk(query(element, '.respectfulReviewTip'));
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
index 9f65dd4..81b6fd4 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.ts
@@ -221,8 +221,10 @@
*
* @param noScroll prevent any potential scrolling in response
* setting the cursor.
+ * @param applyFocus indicates if it should try to focus after move operation
+ * (e.g. focusOnMove).
*/
- setCursor(element: HTMLElement, noScroll?: boolean) {
+ setCursor(element: HTMLElement, noScroll?: boolean, applyFocus?: boolean) {
if (!this.targetableStops.includes(element)) {
this.unsetCursor();
return;
@@ -238,6 +240,9 @@
this._updateIndex();
this._decorateTarget();
+ if (applyFocus) {
+ this._focusAfterMove();
+ }
if (noScroll && behavior) {
this.scrollMode = behavior;
}
@@ -341,15 +346,17 @@
this._targetHeight = this.target.scrollHeight;
}
- if (this.focusOnMove) {
- this.target.focus();
- }
-
this._decorateTarget();
-
+ this._focusAfterMove();
return clipped ? CursorMoveResult.CLIPPED : CursorMoveResult.MOVED;
}
+ _focusAfterMove() {
+ if (this.focusOnMove) {
+ this.target?.focus();
+ }
+ }
+
_decorateTarget() {
if (this.target && this.cursorTargetClass) {
this.target.classList.add(this.cursorTargetClass);
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
index 99f9265..497dc94 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
@@ -30,7 +30,7 @@
import {TimeFormat, DateFormat} from '../../../constants/constants';
import {assertNever} from '../../../utils/common-util';
import {Timestamp} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
const TimeFormats = {
TIME_12: 'h:mm A', // 2:14 PM
@@ -107,7 +107,7 @@
@property({type: Boolean})
relativeOptionNoAgo = false;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
index 97ee39e..1b094c1 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
@@ -130,7 +130,6 @@
</div>
</main>
<footer>
- <slot name="footer"></slot>
<gr-button
id="cancel"
class="${this.cancelLabel.length ? '' : 'hidden'}"
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 e560773..23d9693 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
@@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Subscription} from 'rxjs';
import '@polymer/iron-input/iron-input';
import '../../../styles/shared-styles';
import '../gr-button/gr-button';
@@ -23,7 +24,7 @@
import {customElement, property} from '@polymer/decorators';
import {DiffPreferencesInfo, IgnoreWhitespaceType} from '../../../types/diff';
import {GrSelect} from '../gr-select/gr-select';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
export interface GrDiffPreferences {
$: {
@@ -39,7 +40,7 @@
contextSelect: GrSelect;
ignoreWhiteSpace: HTMLInputElement;
};
- save(): Promise<void>;
+ save(): void;
}
@customElement('gr-diff-preferences')
@@ -54,12 +55,25 @@
@property({type: Object})
diffPrefs?: DiffPreferencesInfo;
- private readonly restApiService = appContext.restApiService;
+ private readonly userModel = getAppContext().userModel;
- loadData() {
- return this.restApiService.getDiffPreferences().then(prefs => {
- this.diffPrefs = prefs;
- });
+ private subscriptions: Subscription[] = [];
+
+ override connectedCallback() {
+ super.connectedCallback();
+ this.subscriptions.push(
+ this.userModel.diffPreferences$.subscribe(diffPreferences => {
+ this.diffPrefs = diffPreferences;
+ })
+ );
+ }
+
+ override disconnectedCallback() {
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions = [];
+ super.disconnectedCallback();
}
_handleDiffPrefsChanged() {
@@ -125,12 +139,10 @@
this._handleDiffPrefsChanged();
}
- save() {
- if (!this.diffPrefs)
- return Promise.reject(new Error('Missing diff preferences'));
- return this.restApiService.saveDiffPreferences(this.diffPrefs).then(_ => {
- this.hasUnsavedChanges = false;
- });
+ async save() {
+ if (!this.diffPrefs) return;
+ await this.userModel.updateDiffPreference(this.diffPrefs);
+ this.hasUnsavedChanges = false;
}
/**
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts
index 6c1404e..8abd679 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.ts
@@ -51,11 +51,113 @@
element = basicFixture.instantiate();
- await element.loadData();
await flush();
});
test('renders', () => {
+ expect(element).shadowDom.to.equal(`<div
+ class="gr-form-styles"
+ id="diffPreferences"
+ >
+ <section>
+ <label class="title" for="contextLineSelect">Context</label>
+ <span class="value">
+ <gr-select id="contextSelect">
+ <select id="contextLineSelect">
+ <option value="3">3 lines</option>
+ <option value="10">10 lines</option>
+ <option value="25">25 lines</option>
+ <option value="50">50 lines</option>
+ <option value="75">75 lines</option>
+ <option value="100">100 lines</option>
+ <option value="-1">Whole file</option>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
+ <label class="title" for="lineWrappingInput">Fit to screen</label>
+ <span class="value">
+ <input id="lineWrappingInput" type="checkbox">
+ </span>
+ </section>
+ <section>
+ <label class="title" for="columnsInput">Diff width</label>
+ <span class="value">
+ <iron-input allowed-pattern="[0-9]">
+ <input id="columnsInput" type="number">
+ </iron-input>
+ </span>
+ </section>
+ <section>
+ <label class="title" for="tabSizeInput">Tab width</label>
+ <span class="value">
+ <iron-input allowed-pattern="[0-9]">
+ <input id="tabSizeInput" type="number">
+ </iron-input>
+ </span>
+ </section>
+ <section>
+ <label class="title" for="fontSizeInput">Font size</label>
+ <span class="value">
+ <iron-input allowed-pattern="[0-9]">
+ <input id="fontSizeInput" type="number">
+ </iron-input>
+ </span>
+ </section>
+ <section>
+ <label class="title" for="showTabsInput">Show tabs</label>
+ <span class="value">
+ <input id="showTabsInput" type="checkbox">
+ </span>
+ </section>
+ <section>
+ <label class="title" for="showTrailingWhitespaceInput">
+ Show trailing whitespace
+ </label>
+ <span class="value">
+ <input id="showTrailingWhitespaceInput" type="checkbox">
+ </span>
+ </section>
+ <section>
+ <label class="title" for="syntaxHighlightInput">
+ Syntax highlighting
+ </label>
+ <span class="value">
+ <input id="syntaxHighlightInput" type="checkbox">
+ </span>
+ </section>
+ <section>
+ <label class="title" for="automaticReviewInput">
+ Automatically mark viewed files reviewed
+ </label>
+ <span class="value">
+ <input id="automaticReviewInput" type="checkbox">
+ </span>
+ </section>
+ <section>
+ <div class="pref">
+ <label class="title" for="ignoreWhiteSpace">
+ Ignore Whitespace
+ </label>
+ <span class="value">
+ <gr-select>
+ <select id="ignoreWhiteSpace">
+ <option value="IGNORE_NONE">None</option>
+ <option value="IGNORE_TRAILING">Trailing</option>
+ <option value="IGNORE_LEADING_AND_TRAILING">
+ Leading & trailing
+ </option>
+ <option value="IGNORE_ALL">All</option>
+ </select>
+ </gr-select>
+ </span>
+ </div>
+ </section>
+ </div>`);
+ });
+
+ test('renders preferences', () => {
// Rendered with the expected preferences selected.
const contextInput = valueOf('Context', 'diffPreferences')
.firstElementChild as IronInputElement;
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 8322682..6eb19da 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
@@ -14,20 +14,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Subscription} from 'rxjs';
import '@polymer/paper-tabs/paper-tab';
import '@polymer/paper-tabs/paper-tabs';
import '../gr-shell-command/gr-shell-command';
+import '../../../styles/gr-paper-styles';
import '../../../styles/shared-styles';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-download-commands_html';
import {customElement, property} from '@polymer/decorators';
import {PaperTabsElement} from '@polymer/paper-tabs/paper-tabs';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {queryAndAssert} from '../../../utils/common-util';
import {GrShellCommand} from '../gr-shell-command/gr-shell-command';
-import {preferences$} from '../../../services/user/user-model';
-import {takeUntil} from 'rxjs/operators';
-import {Subject} from 'rxjs';
declare global {
interface HTMLElementEventMap {
@@ -71,27 +70,33 @@
@property({type: Boolean})
showKeyboardShortcutTooltips = false;
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
- private readonly userService = appContext.userService;
+ // Private but used in tests.
+ readonly userModel = getAppContext().userModel;
- disconnected$ = new Subject();
+ private subscriptions: Subscription[] = [];
override connectedCallback() {
super.connectedCallback();
this._getLoggedIn().then(loggedIn => {
this._loggedIn = loggedIn;
});
- preferences$.pipe(takeUntil(this.disconnected$)).subscribe(prefs => {
- if (prefs?.download_scheme) {
- // Note (issue 5180): normalize the download scheme with lower-case.
- this.selectedScheme = prefs.download_scheme.toLowerCase();
- }
- });
+ this.subscriptions.push(
+ this.userModel.preferences$.subscribe(prefs => {
+ if (prefs?.download_scheme) {
+ // Note (issue 5180): normalize the download scheme with lower-case.
+ this.selectedScheme = prefs.download_scheme.toLowerCase();
+ }
+ })
+ );
}
override disconnectedCallback() {
- this.disconnected$.next();
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions = [];
super.disconnectedCallback();
}
@@ -108,7 +113,7 @@
if (scheme && scheme !== this.selectedScheme) {
this.set('selectedScheme', scheme);
if (this._loggedIn) {
- this.userService.updatePreferences({
+ this.userModel.updatePreferences({
download_scheme: this.selectedScheme,
});
}
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.ts b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.ts
index 5a75c13..f9c08ba 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.ts
@@ -17,6 +17,9 @@
import {html} from '@polymer/polymer/lib/utils/html-tag';
export const htmlTemplate = html`
+ <style include="gr-paper-styles">
+ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ </style>
<style include="shared-styles">
paper-tabs {
height: 3rem;
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 ef712ac..bd0ca70 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
@@ -19,7 +19,6 @@
import './gr-download-commands';
import {GrDownloadCommands} from './gr-download-commands';
import {isHidden, queryAndAssert, stubRestApi} from '../../../test/test-utils';
-import {updatePreferences} from '../../../services/user/user-model';
import {createPreferences} from '../../../test/test-data-generators';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import {GrShellCommand} from '../gr-shell-command/gr-shell-command';
@@ -114,22 +113,22 @@
});
suite('authenticated', () => {
test('loads scheme from preferences', async () => {
- updatePreferences({
+ const element = basicFixture.instantiate();
+ await flush();
+ element.userModel.setPreferences({
...createPreferences(),
download_scheme: 'repo',
});
- const element = basicFixture.instantiate();
- await flush();
assert.equal(element.selectedScheme, 'repo');
});
test('normalize scheme from preferences', async () => {
- updatePreferences({
+ const element = basicFixture.instantiate();
+ await flush();
+ element.userModel.setPreferences({
...createPreferences(),
download_scheme: 'REPO',
});
- const element = basicFixture.instantiate();
- await flush();
assert.equal(element.selectedScheme, 'repo');
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
index 6180f35..a39d033 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
@@ -78,7 +78,7 @@
@property({type: Number})
initialCount = 75;
- @property({type: Object})
+ @property({type: Array})
items?: DropdownItem[];
@property({type: String})
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts
index 2b56de6..4045b6d 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts
@@ -133,19 +133,19 @@
override connectedCallback() {
super.connectedCallback();
this.cleanups.push(
- addShortcut(this, {key: Key.UP}, e => this._handleUp(e))
+ addShortcut(this, {key: Key.UP}, () => this._handleUp())
);
this.cleanups.push(
- addShortcut(this, {key: Key.DOWN}, e => this._handleDown(e))
+ addShortcut(this, {key: Key.DOWN}, () => this._handleDown())
);
this.cleanups.push(
- addShortcut(this, {key: Key.TAB}, e => this._handleTab(e))
+ addShortcut(this, {key: Key.TAB}, () => this._handleTab())
);
this.cleanups.push(
- addShortcut(this, {key: Key.ENTER}, e => this._handleEnter(e))
+ addShortcut(this, {key: Key.ENTER}, () => this._handleEnter())
);
this.cleanups.push(
- addShortcut(this, {key: Key.SPACE}, e => this._handleEnter(e))
+ addShortcut(this, {key: Key.SPACE}, () => this._handleEnter())
);
}
@@ -159,10 +159,8 @@
/**
* Handle the up key.
*/
- _handleUp(e: Event) {
+ _handleUp() {
if (this.$.dropdown.opened) {
- e.preventDefault();
- e.stopPropagation();
this.cursor.previous();
} else {
this._open();
@@ -172,10 +170,8 @@
/**
* Handle the down key.
*/
- _handleDown(e: Event) {
+ _handleDown() {
if (this.$.dropdown.opened) {
- e.preventDefault();
- e.stopPropagation();
this.cursor.next();
} else {
this._open();
@@ -185,20 +181,14 @@
/**
* Handle the tab key.
*/
- _handleTab(e: Event) {
- if (this.$.dropdown.opened) {
- // Tab in a native select is a no-op. Emulate this.
- e.preventDefault();
- e.stopPropagation();
- }
+ _handleTab() {
+ // Tab in a native select is a no-op. Emulate this.
}
/**
* Handle the enter key.
*/
- _handleEnter(e: Event) {
- e.preventDefault();
- e.stopPropagation();
+ _handleEnter() {
if (this.$.dropdown.opened) {
// TODO(milutin): This solution is not particularly robust in general.
// Since gr-tooltip-content click on shadow dom is not propagated down,
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.ts b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.ts
index 3c07d94..082a10b 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.ts
@@ -58,6 +58,7 @@
padding: var(--spacing-m) var(--spacing-l);
}
li .itemAction {
+ color: var(--gr-dropdown-item-color);
@apply --gr-dropdown-item;
}
li .itemAction.disabled {
@@ -83,6 +84,7 @@
.topContent {
display: block;
padding: var(--spacing-m) var(--spacing-l);
+ color: var(--gr-dropdown-item-color);
@apply --gr-dropdown-item;
}
.bold-text {
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 866ee5a..6191bd7 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
@@ -17,11 +17,14 @@
import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
import '../../../styles/shared-styles';
import '../gr-button/gr-button';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param';
+import '../../plugins/gr-endpoint-slot/gr-endpoint-slot';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {customElement, property} from '@polymer/decorators';
import {htmlTemplate} from './gr-editable-content_html';
import {fireAlert, fireEvent} from '../../../utils/event-util';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {debounce, DelayedTask} from '../../../utils/async-util';
import {queryAndAssert} from '../../../utils/common-util';
import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
@@ -66,7 +69,12 @@
@property({type: Boolean, reflectToAttribute: true})
disabled = false;
- @property({type: Boolean, observer: '_editingChanged', notify: true})
+ @property({
+ type: Boolean,
+ observer: '_editingChanged',
+ notify: true,
+ reflectToAttribute: true,
+ })
editing = false;
@property({type: Boolean})
@@ -108,9 +116,9 @@
@property({type: String, observer: '_newContentChanged'})
_newContent = '';
- private readonly storage = appContext.storageService;
+ private readonly storage = getAppContext().storageService;
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
// Tests use this so needs to be non private
storeTask?: DelayedTask;
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
index 7877a1f..8c40177 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.ts
@@ -62,10 +62,9 @@
background-color: var(--view-background-color);
display: flex;
justify-content: flex-end;
- border-top-width: 1px;
- border-top-style: solid;
+ border: 1px solid transparent;
+ border-top-color: var(--border-color);
border-radius: 0 0 4px 4px;
- border-color: var(--border-color);
box-shadow: var(--elevation-level-1);
/* slightly up to cover rounded corner of the commit msg */
margin-top: calc(-1 * var(--spacing-xs));
@@ -73,6 +72,10 @@
*/
position: relative;
}
+ :host([editing]) .show-all-container {
+ box-shadow: none;
+ border: 1px solid var(--border-color);
+ }
.show-all-container .show-all-button {
margin-right: auto;
}
@@ -93,61 +96,65 @@
padding: var(--spacing-xs);
}
</style>
- <div
- class="viewer"
- hidden$="[[editing]]"
- collapsed$="[[_computeCommitMessageCollapsed(_commitCollapsed, commitCollapsible)]]"
- >
- <slot></slot>
- </div>
- <div class="editor" hidden$="[[!editing]]">
- <div>
- <iron-autogrow-textarea
- autocomplete="on"
- bind-value="{{_newContent}}"
- disabled="[[disabled]]"
- ></iron-autogrow-textarea>
- </div>
- </div>
- <div class="show-all-container" hidden$="[[_hideShowAllContainer]]">
- <gr-button
- link=""
- class="show-all-button"
- on-click="_toggleCommitCollapsed"
- hidden$="[[_hideShowAllButton]]"
- ><iron-icon
- icon="gr-icons:expand-more"
- hidden$="[[!_commitCollapsed]]"
- ></iron-icon
- ><iron-icon
- icon="gr-icons:expand-less"
- hidden$="[[_commitCollapsed]]"
- ></iron-icon>
- [[_computeCollapseText(_commitCollapsed)]]
- </gr-button>
- <gr-button
- link=""
- class="edit-commit-message"
- title="Edit commit message"
- on-click="_handleEditCommitMessage"
- hidden$="[[hideEditCommitMessage]]"
- ><iron-icon icon="gr-icons:edit"></iron-icon> Edit</gr-button
+ <gr-endpoint-decorator name="commit-message">
+ <gr-endpoint-param name="editing" value="[[editing]]"></gr-endpoint-param>
+ <div
+ class="viewer"
+ hidden$="[[editing]]"
+ collapsed$="[[_computeCommitMessageCollapsed(_commitCollapsed, commitCollapsible)]]"
>
- <div class="editButtons" hidden$="[[!editing]]">
+ <slot></slot>
+ </div>
+ <div class="editor" hidden$="[[!editing]]">
+ <div>
+ <iron-autogrow-textarea
+ autocomplete="on"
+ bind-value="{{_newContent}}"
+ disabled="[[disabled]]"
+ ></iron-autogrow-textarea>
+ </div>
+ </div>
+ <gr-endpoint-slot name="above-actions"></gr-endpoint-slot>
+ <div class="show-all-container" hidden$="[[_hideShowAllContainer]]">
<gr-button
link=""
- class="cancel-button"
- on-click="_handleCancel"
- disabled="[[disabled]]"
- >Cancel</gr-button
- >
+ class="show-all-button"
+ on-click="_toggleCommitCollapsed"
+ hidden$="[[_hideShowAllButton]]"
+ ><iron-icon
+ icon="gr-icons:expand-more"
+ hidden$="[[!_commitCollapsed]]"
+ ></iron-icon
+ ><iron-icon
+ icon="gr-icons:expand-less"
+ hidden$="[[_commitCollapsed]]"
+ ></iron-icon>
+ [[_computeCollapseText(_commitCollapsed)]]
+ </gr-button>
<gr-button
- class="save-button"
- primary=""
- on-click="_handleSave"
- disabled="[[_saveDisabled]]"
- >Save</gr-button
+ link=""
+ class="edit-commit-message"
+ title="Edit commit message"
+ on-click="_handleEditCommitMessage"
+ hidden$="[[hideEditCommitMessage]]"
+ ><iron-icon icon="gr-icons:edit"></iron-icon> Edit</gr-button
>
+ <div class="editButtons" hidden$="[[!editing]]">
+ <gr-button
+ link=""
+ class="cancel-button"
+ on-click="_handleCancel"
+ disabled="[[disabled]]"
+ >Cancel</gr-button
+ >
+ <gr-button
+ class="save-button"
+ primary=""
+ on-click="_handleSave"
+ disabled="[[_saveDisabled]]"
+ >Save</gr-button
+ >
+ </div>
</div>
- </div>
+ </gr-endpoint-decorator>
`;
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
index e0d1d15..8564751 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.ts
@@ -213,12 +213,15 @@
}
_handleEnter(event: KeyboardEvent) {
+ const grAutocomplete = this.getGrAutocomplete();
+ if (event.composedPath().some(el => el === grAutocomplete)) {
+ return;
+ }
const inputContainer = queryAndAssert(this, '.inputContainer');
const isEventFromInput = event
.composedPath()
.some(element => element === inputContainer);
if (isEventFromInput) {
- event.preventDefault();
this._save();
}
}
@@ -229,13 +232,12 @@
.composedPath()
.some(element => element === inputContainer);
if (isEventFromInput) {
- event.preventDefault();
this._cancel();
}
}
_handleCommit() {
- this._save();
+ this.getInput()?.focus();
}
_computeLabelClass(readOnly?: boolean, value?: string, placeholder?: string) {
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 6a34fbb..c79c7f3 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.ts
@@ -18,7 +18,7 @@
import '@polymer/iron-icon/iron-icon';
import '../gr-avatar/gr-avatar';
import '../gr-button/gr-button';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {accountKey, isSelf} from '../../../utils/account-util';
import {customElement, property} from 'lit/decorators';
import {
@@ -27,7 +27,6 @@
ServerInfo,
ReviewInput,
} from '../../../types/common';
-import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
import {
canHaveAttention,
getAddedByReason,
@@ -81,14 +80,9 @@
@property({type: Object})
_config?: ServerInfo;
- reporting: ReportingService;
+ private readonly restApiService = getAppContext().restApiService;
- private readonly restApiService = appContext.restApiService;
-
- constructor() {
- super();
- this.reporting = appContext.reportingService;
- }
+ private readonly reporting = getAppContext().reportingService;
override connectedCallback() {
super.connectedCallback();
@@ -171,7 +165,7 @@
return html`
<div class="top">
<div class="avatar">
- <gr-avautar .account=${this.account} imageSize="56"></gr-avatar>
+ <gr-avatar .account=${this.account} imageSize="56"></gr-avatar>
</div>
<div class="account">
<h3 class="name heading-3">${this.account.name}</h3>
@@ -179,16 +173,14 @@
</div>
</div>
${this.renderAccountStatus()}
- ${
- this.voteableText
- ? html`
- <div class="voteable">
- <span class="title">Voteable:</span>
- <span class="value">${this.voteableText}</span>
- </div>
- `
- : ''
- }
+ ${this.voteableText
+ ? html`
+ <div class="voteable">
+ <span class="title">Voteable:</span>
+ <span class="value">${this.voteableText}</span>
+ </div>
+ `
+ : ''}
${this.renderNeedsAttention()} ${this.renderAddToAttention()}
${this.renderRemoveFromAttention()} ${this.renderReviewerOrCcActions()}
`;
@@ -226,7 +218,7 @@
return html`
<div class="status">
<span class="title">
- <iron-icon icon="gr-icons:calendar"></iron-icon>
+ <iron-icon icon="gr-icons:unavailable"></iron-icon>
Status:
</span>
<span class="value">${this.account.status}</span>
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
deleted file mode 100644
index 5530d7c..0000000
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
+++ /dev/null
@@ -1,255 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-hovercard-account.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-import {ReviewerState} from '../../../constants/constants.js';
-import {mockPromise, stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromTemplate(html`
-<gr-hovercard-account class="hovered"></gr-hovercard-account>
-`);
-
-suite('gr-hovercard-account tests', () => {
- let element;
-
- const ACCOUNT = {
- email: 'kermit@gmail.com',
- username: 'kermit',
- name: 'Kermit The Frog',
- _account_id: '31415926535',
- };
-
- setup(async () => {
- stubRestApi('getAccount').returns(Promise.resolve({...ACCOUNT}));
- element = basicFixture.instantiate();
- element.account = {...ACCOUNT};
- element.change = {
- attention_set: {},
- reviewers: {},
- owner: {...ACCOUNT},
- };
- element.show({});
- await flush();
- });
-
- teardown(() => {
- element.hide({});
- });
-
- test('account name is shown', () => {
- assert.equal(element.shadowRoot.querySelector('.name').innerText,
- 'Kermit The Frog');
- });
-
- test('computePronoun', () => {
- element.account = {_account_id: '1'};
- element._selfAccount = {_account_id: '1'};
- assert.equal(element.computePronoun(), 'Your');
- element.account = {_account_id: '2'};
- assert.equal(element.computePronoun(), 'Their');
- });
-
- test('account status is not shown if the property is not set', () => {
- assert.isNull(element.shadowRoot.querySelector('.status'));
- });
-
- test('account status is displayed', async () => {
- element.account = {status: 'OOO', ...ACCOUNT};
- await element.updateComplete;
- assert.equal(element.shadowRoot.querySelector('.status .value').innerText,
- 'OOO');
- });
-
- test('voteable div is not shown if the property is not set', () => {
- assert.isNull(element.shadowRoot.querySelector('.voteable'));
- });
-
- test('voteable div is displayed', async () => {
- element.voteableText = 'CodeReview: +2';
- await element.updateComplete;
- assert.equal(element.shadowRoot.querySelector('.voteable .value').innerText,
- element.voteableText);
- });
-
- test('remove reviewer', async () => {
- element.change = {
- removable_reviewers: [ACCOUNT],
- reviewers: {
- [ReviewerState.REVIEWER]: [ACCOUNT],
- },
- };
- await element.updateComplete;
- stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
- const reloadListener = sinon.spy();
- element._target.addEventListener('reload', reloadListener);
- const button = element.shadowRoot.querySelector('.removeReviewerOrCC');
- assert.isOk(button);
- assert.equal(button.innerText, 'Remove Reviewer');
- MockInteractions.tap(button);
- await element.updateComplete;
- assert.isTrue(reloadListener.called);
- });
-
- test('move reviewer to cc', async () => {
- element.change = {
- removable_reviewers: [ACCOUNT],
- reviewers: {
- [ReviewerState.REVIEWER]: [ACCOUNT],
- },
- };
- await element.updateComplete;
- const saveReviewStub = stubRestApi(
- 'saveChangeReview').returns(
- Promise.resolve({ok: true}));
- stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
- const reloadListener = sinon.spy();
- element._target.addEventListener('reload', reloadListener);
-
- const button = element.shadowRoot.querySelector('.changeReviewerOrCC');
-
- assert.isOk(button);
- assert.equal(button.innerText, 'Move Reviewer to CC');
- MockInteractions.tap(button);
- await element.updateComplete;
- assert.isTrue(saveReviewStub.called);
- assert.isTrue(reloadListener.called);
- });
-
- test('move reviewer to cc', async () => {
- element.change = {
- removable_reviewers: [ACCOUNT],
- reviewers: {
- [ReviewerState.REVIEWER]: [],
- },
- };
- await element.updateComplete;
- const saveReviewStub = stubRestApi(
- 'saveChangeReview').returns(Promise.resolve({ok: true}));
- stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
- const reloadListener = sinon.spy();
- element._target.addEventListener('reload', reloadListener);
-
- const button = element.shadowRoot.querySelector('.changeReviewerOrCC');
- assert.isOk(button);
- assert.equal(button.innerText, 'Move CC to Reviewer');
-
- MockInteractions.tap(button);
- await element.updateComplete;
- assert.isTrue(saveReviewStub.called);
- assert.isTrue(reloadListener.called);
- });
-
- test('remove cc', async () => {
- element.change = {
- removable_reviewers: [ACCOUNT],
- reviewers: {
- [ReviewerState.REVIEWER]: [],
- },
- };
- await element.updateComplete;
- stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
- const reloadListener = sinon.spy();
- element._target.addEventListener('reload', reloadListener);
-
- const button = element.shadowRoot.querySelector('.removeReviewerOrCC');
-
- assert.equal(button.innerText, 'Remove CC');
- assert.isOk(button);
- MockInteractions.tap(button);
- await element.updateComplete;
- assert.isTrue(reloadListener.called);
- });
-
- test('add to attention set', async () => {
- const apiPromise = mockPromise();
- 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('show-alert', showAlertListener);
- element._target.addEventListener('hide-alert', hideAlertListener);
- element._target.addEventListener('attention-set-updated', updatedListener);
-
- const button = element.shadowRoot.querySelector('.addToAttentionSet');
- assert.isOk(button);
- assert.isTrue(element._isShowing, 'hovercard is showing');
- MockInteractions.tap(button);
-
- 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({});
- 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();
- const apiSpy = stubRestApi('removeFromAttentionSet').returns(apiPromise);
- element.highlightAttention = true;
- element.change = {
- attention_set: {31415926535: {}},
- 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('show-alert', showAlertListener);
- element._target.addEventListener('hide-alert', hideAlertListener);
- element._target.addEventListener('attention-set-updated', updatedListener);
-
- const button = element.shadowRoot.querySelector('.removeFromAttentionSet');
- assert.isOk(button);
- assert.isTrue(element._isShowing, 'hovercard is showing');
- MockInteractions.tap(button);
-
- 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({});
- 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_test.ts b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts
new file mode 100644
index 0000000..66a4e1b
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.ts
@@ -0,0 +1,331 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import {fixture} from '@open-wc/testing-helpers';
+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.js';
+import {
+ createAccountDetailWithId,
+ createChange,
+} from '../../../test/test-data-generators.js';
+import {GrButton} from '../gr-button/gr-button.js';
+
+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',
+ _account_id: 31415926535 as AccountId,
+ };
+
+ setup(async () => {
+ stubRestApi('getAccount').returns(Promise.resolve({...ACCOUNT}));
+ const change = {
+ ...createChange(),
+ attention_set: {},
+ reviewers: {},
+ owner: {...ACCOUNT},
+ };
+ element = await fixture<GrHovercardAccount>(
+ html`<gr-hovercard-account
+ class="hovered"
+ .account=${ACCOUNT}
+ .change=${change}
+ >
+ </gr-hovercard-account>`
+ );
+ await element.show();
+ await element.updateComplete;
+ });
+
+ teardown(async () => {
+ await element.hide(new MouseEvent('click'));
+ await element.updateComplete;
+ });
+
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(`<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>
+ </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', () => {
+ assert.isUndefined(query(element, '.status'));
+ });
+
+ test('account status is displayed', async () => {
+ element.account = {...ACCOUNT, status: 'OOO'};
+ await element.updateComplete;
+ const status = queryAndAssert<HTMLSpanElement>(element, '.status .value');
+ assert.equal(status.innerText, 'OOO');
+ });
+
+ test('voteable div is not shown if the property is not set', () => {
+ assert.isUndefined(query(element, '.voteable'));
+ });
+
+ test('voteable div is displayed', async () => {
+ element.voteableText = 'CodeReview: +2';
+ await element.updateComplete;
+ const voteableEl = queryAndAssert<HTMLSpanElement>(
+ element,
+ '.voteable .value'
+ );
+ assert.equal(voteableEl.innerText, element.voteableText);
+ });
+
+ 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('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('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');
+ });
+});
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 1a6239f..4456381 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.ts
@@ -83,6 +83,10 @@
<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://material.io/icons/#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 -->
@@ -162,6 +166,12 @@
<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>
</defs>
</svg>
</iron-iconset-svg>`;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts
index 82c7118..3add34d 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api.ts
@@ -17,7 +17,7 @@
import {DiffLayer, DiffLayerListener} from '../../../types/types';
import {Side} from '../../../constants/constants';
import {EventType, PluginApi} from '../../../api/plugin';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {AnnotationPluginApi, CoverageProvider} from '../../../api/annotation';
export class GrAnnotationActionsInterface implements AnnotationPluginApi {
@@ -30,7 +30,7 @@
private coverageProvider?: CoverageProvider;
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
constructor(private readonly plugin: PluginApi) {
this.reporting.trackApi(this.plugin, 'annotation', 'constructor');
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.js
index 996edf3..1088b27 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.js
@@ -17,9 +17,6 @@
import '../../../test/common-test-setup-karma.js';
import '../../change/gr-change-actions/gr-change-actions.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
-
-const pluginApi = _testOnly_initGerritPluginApi();
suite('gr-annotation-actions-js-api tests', () => {
let annotationActions;
@@ -27,7 +24,7 @@
let plugin;
setup(() => {
- pluginApi.install(p => { plugin = p; }, '0.1',
+ window.Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
annotationActions = plugin.annotationApi();
});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.ts
index 36e5c25..e3397ea 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.ts
@@ -18,7 +18,7 @@
import {getBaseUrl} from '../../../utils/url-util';
import {HttpMethod} from '../../../constants/constants';
import {RequestPayload} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
+import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
export const PLUGIN_LOADING_TIMEOUT_MS = 10000;
@@ -62,14 +62,14 @@
return pathname.split('/')[2].split('.')[0];
}
-// TODO(taoalpha): to be deprecated.
export function send(
+ restApiService: RestApiService,
method: HttpMethod,
url: string,
opt_callback?: (response: unknown) => void,
opt_payload?: RequestPayload
) {
- return appContext.restApiService
+ return restApiService
.send(method, url, opt_payload)
.then(response => {
if (response.status < 200 || response.status >= 300) {
@@ -81,7 +81,7 @@
}
});
} else {
- return appContext.restApiService.getResponseObject(response);
+ return restApiService.getResponseObject(response);
}
})
.then(response => {
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 a33b145..36e2280 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
@@ -16,7 +16,7 @@
*/
import {PluginApi, TargetElement} from '../../../api/plugin';
import {ActionInfo, RequireProperties} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {
ActionPriority,
ActionType,
@@ -68,7 +68,9 @@
ActionType = ActionType;
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
+
+ private readonly jsApiService = getAppContext().jsApiService;
constructor(public plugin: PluginApi, el?: GrChangeActionsElement) {
this.reporting.trackApi(this.plugin, 'actions', 'constructor');
@@ -92,7 +94,7 @@
*/
ensureEl(): GrChangeActionsElement {
if (!this.el) {
- const sharedApiElement = appContext.jsApiService;
+ const sharedApiElement = this.jsApiService;
this.setEl(
sharedApiElement.getElement(
TargetElement.CHANGE_ACTIONS
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
index 87f6052..b70c8ca 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.js
@@ -19,12 +19,8 @@
import '../../change/gr-change-actions/gr-change-actions.js';
import {resetPlugins} from '../../../test/test-utils.js';
import {getPluginLoader} from './gr-plugin-loader.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
-
const basicFixture = fixtureFromElement('gr-change-actions');
-const pluginApi = _testOnly_initGerritPluginApi();
-
suite('gr-change-actions-js-api-interface tests', () => {
let element;
let changeActions;
@@ -41,7 +37,7 @@
suite('early init', () => {
setup(() => {
resetPlugins();
- pluginApi.install(p => { plugin = p; }, '0.1',
+ window.Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
// Mimic all plugins loaded.
getPluginLoader().loadPlugins([]);
@@ -68,7 +64,7 @@
sinon.stub(element, '_editStatusChanged');
element.change = {};
element._hasKnownChainState = false;
- pluginApi.install(p => { plugin = p; }, '0.1',
+ window.Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
changeActions = plugin.changeActions();
// Mimic all plugins loaded.
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.ts
index aa86de4..b93bc4a 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.ts
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-import {GrReplyDialog} from '../../../services/gr-rest-api/gr-rest-api';
+import {GrReplyDialog} from '../../change/gr-reply-dialog/gr-reply-dialog';
import {PluginApi, TargetElement} from '../../../api/plugin';
import {JsApiService} from './gr-js-api-types';
import {
@@ -25,14 +25,14 @@
ReplyChangedCallback,
ValueChangedDetail,
} from '../../../api/change-reply';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {HookApi, PluginElement} from '../../../api/hook';
/**
* GrChangeReplyInterface, provides a set of handy methods on reply dialog.
*/
export class GrChangeReplyInterface implements ChangeReplyPluginApi {
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
constructor(
readonly plugin: PluginApi,
@@ -47,7 +47,7 @@
) as unknown as GrReplyDialog;
}
- getLabelValue(label: string): string {
+ getLabelValue(label: string): string | number | undefined {
this.reporting.trackApi(this.plugin, 'reply', 'getLabelValue');
return this._el.getLabelValue(label);
}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.js
index 2324588..52d6ab3 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.js
@@ -17,16 +17,12 @@
import '../../../test/common-test-setup-karma.js';
import '../../change/gr-reply-dialog/gr-reply-dialog.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
import {stubRestApi} from '../../../test/test-utils.js';
const basicFixture = fixtureFromElement('gr-reply-dialog');
-const pluginApi = _testOnly_initGerritPluginApi();
-
suite('gr-change-reply-js-api tests', () => {
let element;
-
let changeReply;
let plugin;
@@ -36,7 +32,7 @@
suite('early init', () => {
setup(() => {
- pluginApi.install(p => { plugin = p; }, '0.1',
+ window.Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
changeReply = plugin.changeReply();
element = basicFixture.instantiate();
@@ -64,7 +60,7 @@
suite('normal init', () => {
setup(() => {
element = basicFixture.instantiate();
- pluginApi.install(p => { plugin = p; }, '0.1',
+ window.Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
changeReply = plugin.changeReply();
});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
index 1d4bd06..07fad80 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
@@ -21,8 +21,11 @@
*/
import {getPluginLoader, PluginOptionMap} from './gr-plugin-loader';
import {send} from './gr-api-utils';
-import {appContext} from '../../../services/app-context';
+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 {
@@ -37,6 +40,7 @@
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';
/**
* These are the methods and properties that are exposed explicitly in the
@@ -74,15 +78,15 @@
// exposed methods
Nav: typeof GerritNav;
- Auth: typeof appContext.authService;
+ Auth: AuthService;
}
-export function initGerritPluginApi() {
- window.Gerrit = window.Gerrit ?? new GerritImpl();
+export function initGerritPluginApi(appContext: AppContext) {
+ window.Gerrit = window.Gerrit ?? new GerritImpl(appContext);
}
-export function _testOnly_initGerritPluginApi(): GerritInternal {
- initGerritPluginApi();
+export function _testOnly_getGerritInternalPluginApi(): GerritInternal {
+ if (!window.Gerrit) throw new Error('initGerritPluginApi was not called');
return window.Gerrit as GerritInternal;
}
@@ -91,8 +95,8 @@
callback?: (response: Response) => void
) {
console.warn('.delete() is deprecated! Use plugin.restApi().delete()');
- return appContext.restApiService
- .send(HttpMethod.DELETE, url)
+ return getAppContext()
+ .restApiService.send(HttpMethod.DELETE, url)
.then(response => {
if (response.status !== 204) {
return response.text().then(text => {
@@ -120,7 +124,13 @@
public readonly Nav = GerritNav;
- public readonly Auth = appContext.authService;
+ public readonly Auth: AuthService;
+
+ private readonly reportingService: ReportingService;
+
+ private readonly eventEmitter: EventEmitterService;
+
+ private readonly restApiService: RestApiService;
public readonly styles = {
font: fontStyles,
@@ -131,12 +141,24 @@
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) {
- appContext.reportingService.trackApi(fakeApi, 'global', 'css');
+ this.reportingService.trackApi(fakeApi, 'global', 'css');
console.warn(
'Gerrit.css(rulesStr) is deprecated!',
'Use plugin.styles().css(rulesStr)'
@@ -161,18 +183,18 @@
}
getLoggedIn() {
- appContext.reportingService.trackApi(fakeApi, 'global', 'getLoggedIn');
+ this.reportingService.trackApi(fakeApi, 'global', 'getLoggedIn');
console.warn(
'Gerrit.getLoggedIn() is deprecated! ' +
'Use plugin.restApi().getLoggedIn()'
);
- return appContext.restApiService.getLoggedIn();
+ return this.restApiService.getLoggedIn();
}
get(url: string, callback?: (response: unknown) => void) {
- appContext.reportingService.trackApi(fakeApi, 'global', 'get');
+ this.reportingService.trackApi(fakeApi, 'global', 'get');
console.warn('.get() is deprecated! Use plugin.restApi().get()');
- send(HttpMethod.GET, url, callback);
+ send(this.restApiService, HttpMethod.GET, url, callback);
}
post(
@@ -180,9 +202,9 @@
payload?: RequestPayload,
callback?: (response: unknown) => void
) {
- appContext.reportingService.trackApi(fakeApi, 'global', 'post');
+ this.reportingService.trackApi(fakeApi, 'global', 'post');
console.warn('.post() is deprecated! Use plugin.restApi().post()');
- send(HttpMethod.POST, url, callback, payload);
+ send(this.restApiService, HttpMethod.POST, url, callback, payload);
}
put(
@@ -190,43 +212,35 @@
payload?: RequestPayload,
callback?: (response: unknown) => void
) {
- appContext.reportingService.trackApi(fakeApi, 'global', 'put');
+ this.reportingService.trackApi(fakeApi, 'global', 'put');
console.warn('.put() is deprecated! Use plugin.restApi().put()');
- send(HttpMethod.PUT, url, callback, payload);
+ send(this.restApiService, HttpMethod.PUT, url, callback, payload);
}
delete(url: string, callback?: (response: Response) => void) {
- appContext.reportingService.trackApi(fakeApi, 'global', 'delete');
+ this.reportingService.trackApi(fakeApi, 'global', 'delete');
deprecatedDelete(url, callback);
}
awaitPluginsLoaded() {
- appContext.reportingService.trackApi(
- fakeApi,
- 'global',
- 'awaitPluginsLoaded'
- );
+ this.reportingService.trackApi(fakeApi, 'global', 'awaitPluginsLoaded');
return getPluginLoader().awaitPluginsLoaded();
}
// TODO(taoalpha): consider removing these proxy methods
// and using getPluginLoader() directly
_loadPlugins(plugins: string[] = []) {
- appContext.reportingService.trackApi(fakeApi, 'global', '_loadPlugins');
+ this.reportingService.trackApi(fakeApi, 'global', '_loadPlugins');
getPluginLoader().loadPlugins(plugins);
}
_arePluginsLoaded() {
- appContext.reportingService.trackApi(
- fakeApi,
- 'global',
- '_arePluginsLoaded'
- );
+ this.reportingService.trackApi(fakeApi, 'global', '_arePluginsLoaded');
return getPluginLoader().arePluginsLoaded();
}
_isPluginEnabled(pathOrUrl: string) {
- appContext.reportingService.trackApi(fakeApi, 'global', '_isPluginEnabled');
+ this.reportingService.trackApi(fakeApi, 'global', '_isPluginEnabled');
return getPluginLoader().isPluginEnabled(pathOrUrl);
}
@@ -235,7 +249,7 @@
}
_isPluginLoaded(pathOrUrl: string) {
- appContext.reportingService.trackApi(fakeApi, 'global', '_isPluginLoaded');
+ this.reportingService.trackApi(fakeApi, 'global', '_isPluginLoaded');
return getPluginLoader().isPluginLoaded(pathOrUrl);
}
@@ -262,48 +276,44 @@
* });
*/
addListener(eventName: string, cb: EventCallback) {
- appContext.reportingService.trackApi(fakeApi, 'global', 'addListener');
- return appContext.eventEmitter.addListener(eventName, cb);
+ 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) {
- appContext.reportingService.trackApi(fakeApi, 'global', 'dispatch');
- return appContext.eventEmitter.dispatch(eventName, detail);
+ 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) {
- appContext.reportingService.trackApi(fakeApi, 'global', 'emit');
- return appContext.eventEmitter.emit(eventName, detail);
+ this.reportingService.trackApi(fakeApi, 'global', 'emit');
+ return this.eventEmitter.emit(eventName, detail);
}
off(eventName: string, cb: EventCallback) {
- appContext.reportingService.trackApi(fakeApi, 'global', 'off');
- return appContext.eventEmitter.off(eventName, cb);
+ this.reportingService.trackApi(fakeApi, 'global', 'off');
+ return this.eventEmitter.off(eventName, cb);
}
on(eventName: string, cb: EventCallback) {
- appContext.reportingService.trackApi(fakeApi, 'global', 'on');
- return appContext.eventEmitter.on(eventName, cb);
+ this.reportingService.trackApi(fakeApi, 'global', 'on');
+ return this.eventEmitter.on(eventName, cb);
}
once(eventName: string, cb: EventCallback) {
- appContext.reportingService.trackApi(fakeApi, 'global', 'once');
- return appContext.eventEmitter.once(eventName, cb);
+ this.reportingService.trackApi(fakeApi, 'global', 'once');
+ return this.eventEmitter.once(eventName, cb);
}
removeAllListeners(eventName: string) {
- appContext.reportingService.trackApi(
- fakeApi,
- 'global',
- 'removeAllListeners'
- );
- return appContext.eventEmitter.removeAllListeners(eventName);
+ this.reportingService.trackApi(fakeApi, 'global', 'removeAllListeners');
+ return this.eventEmitter.removeAllListeners(eventName);
}
removeListener(eventName: string, cb: EventCallback) {
- appContext.reportingService.trackApi(fakeApi, 'global', 'removeListener');
- return appContext.eventEmitter.removeListener(eventName, cb);
+ this.reportingService.trackApi(fakeApi, 'global', 'removeListener');
+ return this.eventEmitter.removeListener(eventName, cb);
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.js
index 95a67ab..d53c266 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.js
@@ -18,23 +18,22 @@
import '../../../test/common-test-setup-karma.js';
import {getPluginLoader} from './gr-plugin-loader.js';
import {resetPlugins} from '../../../test/test-utils.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
+import {_testOnly_getGerritInternalPluginApi} from './gr-gerrit.js';
import {stubRestApi} from '../../../test/test-utils.js';
-import {appContext} from '../../../services/app-context.js';
-
-const pluginApi = _testOnly_initGerritPluginApi();
+import {getAppContext} from '../../../services/app-context.js';
suite('gr-gerrit tests', () => {
let element;
-
let clock;
+ let pluginApi;
setup(() => {
clock = sinon.useFakeTimers();
stubRestApi('getAccount').returns(Promise.resolve({name: 'Judy Hopps'}));
stubRestApi('send').returns(Promise.resolve({status: 200}));
- element = appContext.jsApiService;
+ element = getAppContext().jsApiService;
+ pluginApi = _testOnly_getGerritInternalPluginApi();
});
teardown(() => {
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 4bb5fd4..e7354c5 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
@@ -32,14 +32,17 @@
} from './gr-js-api-types';
import {EventType, TargetElement} from '../../../api/plugin';
import {DiffLayer, HighlightJS, ParsedChangeInfo} from '../../../types/types';
-import {appContext} from '../../../services/app-context';
import {MenuLink} from '../../../api/admin';
+import {Finalizable} from '../../../services/registry';
+import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
const elements: {[key: string]: HTMLElement} = {};
const eventCallbacks: {[key: string]: EventCallback[]} = {};
-export class GrJsApiInterface implements JsApiService {
- private readonly reporting = appContext.reportingService;
+export class GrJsApiInterface implements JsApiService, Finalizable {
+ constructor(readonly reporting: ReportingService) {}
+
+ finalize() {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handleEvent(type: EventType, detail: any) {
@@ -95,8 +98,12 @@
const cancelSubmit = submitCallbacks.some(callback => {
try {
return callback(change, revision) === false;
- } catch (err) {
- this.reporting.error(err);
+ } catch (err: unknown) {
+ this.reporting.error(
+ new Error('canSubmitChange callback error'),
+ undefined,
+ err
+ );
}
return false;
});
@@ -116,8 +123,12 @@
for (const cb of this._getEventCallbacks(EventType.HISTORY)) {
try {
cb(detail.path);
- } catch (err) {
- this.reporting.error(err);
+ } catch (err: unknown) {
+ this.reporting.error(
+ new Error('handleHistory callback error'),
+ undefined,
+ err
+ );
}
}
}
@@ -157,8 +168,12 @@
for (const cb of this._getEventCallbacks(EventType.SHOW_CHANGE)) {
try {
cb(change, revision, info);
- } catch (err) {
- this.reporting.error(err);
+ } catch (err: unknown) {
+ this.reporting.error(
+ new Error('showChange callback error'),
+ undefined,
+ err
+ );
}
}
}
@@ -170,8 +185,12 @@
for (const cb of registeredCallbacks) {
try {
cb(detail.revisionActions, detail.change);
- } catch (err) {
- this.reporting.error(err);
+ } catch (err: unknown) {
+ this.reporting.error(
+ new Error('showRevisionActions callback error'),
+ undefined,
+ err
+ );
}
}
}
@@ -180,8 +199,12 @@
for (const cb of this._getEventCallbacks(EventType.COMMIT_MSG_EDIT)) {
try {
cb(change, msg);
- } catch (err) {
- this.reporting.error(err);
+ } catch (err: unknown) {
+ this.reporting.error(
+ new Error('commitMessage callback error'),
+ undefined,
+ err
+ );
}
}
}
@@ -191,8 +214,12 @@
for (const cb of this._getEventCallbacks(EventType.COMMENT)) {
try {
cb(detail.node);
- } catch (err) {
- this.reporting.error(err);
+ } catch (err: unknown) {
+ this.reporting.error(
+ new Error('comment callback error'),
+ undefined,
+ err
+ );
}
}
}
@@ -201,8 +228,12 @@
for (const cb of this._getEventCallbacks(EventType.LABEL_CHANGE)) {
try {
cb(detail.change);
- } catch (err) {
- this.reporting.error(err);
+ } catch (err: unknown) {
+ this.reporting.error(
+ new Error('labelChange callback error'),
+ undefined,
+ err
+ );
}
}
}
@@ -211,8 +242,12 @@
for (const cb of this._getEventCallbacks(EventType.HIGHLIGHTJS_LOADED)) {
try {
cb(detail.hljs);
- } catch (err) {
- this.reporting.error(err);
+ } catch (err: unknown) {
+ this.reporting.error(
+ new Error('HighlightjsLoaded callback error'),
+ undefined,
+ err
+ );
}
}
}
@@ -221,8 +256,12 @@
for (const cb of this._getEventCallbacks(EventType.REVERT)) {
try {
revertMsg = cb(change, revertMsg, origMsg) as string;
- } catch (err) {
- this.reporting.error(err);
+ } catch (err: unknown) {
+ this.reporting.error(
+ new Error('modifyRevertMsg callback error'),
+ undefined,
+ err
+ );
}
}
return revertMsg;
@@ -240,8 +279,12 @@
revertSubmissionMsg,
origMsg
) as string;
- } catch (err) {
- this.reporting.error(err);
+ } catch (err: unknown) {
+ this.reporting.error(
+ new Error('modifyRevertSubmissionMsg callback error'),
+ undefined,
+ err
+ );
}
}
return revertSubmissionMsg;
@@ -254,8 +297,12 @@
try {
const layer = annotationApi.createLayer(path);
if (layer) layers.push(layer);
- } catch (err) {
- this.reporting.error(err);
+ } catch (err: unknown) {
+ this.reporting.error(
+ new Error('getDiffLayers callback error'),
+ undefined,
+ err
+ );
}
}
return layers;
@@ -266,8 +313,12 @@
try {
const annotationApi = cb as unknown as GrAnnotationActionsInterface;
annotationApi.disposeLayer(path);
- } catch (err) {
- this.reporting.error(err);
+ } catch (err: unknown) {
+ this.reporting.error(
+ new Error('disposeDiffLayers callback error'),
+ undefined,
+ err
+ );
}
}
}
@@ -312,8 +363,12 @@
} else {
review = {labels: r as LabelNameToValuesMap};
}
- } catch (err) {
- this.reporting.error(err);
+ } catch (err: unknown) {
+ this.reporting.error(
+ new Error('getReviewPostRevert callback error'),
+ undefined,
+ err
+ );
}
}
return review;
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 29db685..c45bbf5 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
@@ -21,12 +21,9 @@
import {EventType} from '../../../api/plugin.js';
import {PLUGIN_LOADING_TIMEOUT_MS} from './gr-api-utils.js';
import {getPluginLoader} from './gr-plugin-loader.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
import {stubBaseUrl} from '../../../test/test-utils.js';
import {stubRestApi} from '../../../test/test-utils.js';
-import {appContext} from '../../../services/app-context.js';
-
-const pluginApi = _testOnly_initGerritPluginApi();
+import {getAppContext} from '../../../services/app-context.js';
suite('GrJsApiInterface tests', () => {
let element;
@@ -45,9 +42,9 @@
stubRestApi('getAccount').returns(Promise.resolve({name: 'Judy Hopps'}));
sendStub = stubRestApi('send').returns(Promise.resolve({status: 200}));
- element = appContext.jsApiService;
+ element = getAppContext().jsApiService;
errorStub = sinon.stub(element.reporting, 'error');
- pluginApi.install(p => { plugin = p; }, '0.1',
+ window.Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
getPluginLoader().loadPlugins([]);
});
@@ -300,7 +297,7 @@
setup(() => {
stubBaseUrl('/r');
- pluginApi.install(p => { baseUrlPlugin = p; }, '0.1',
+ window.Gerrit.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 9644ef3..7e6a0c7 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
@@ -21,6 +21,7 @@
ReviewInput,
RevisionInfo,
} from '../../../types/common';
+import {Finalizable} from '../../../services/registry';
import {EventType, TargetElement} from '../../../api/plugin';
import {DiffLayer, ParsedChangeInfo} from '../../../types/types';
import {GrAnnotationActionsInterface} from './gr-annotation-actions-js-api';
@@ -40,7 +41,7 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type EventCallback = (...args: any[]) => any;
-export interface JsApiService {
+export interface JsApiService extends Finalizable {
getElement(key: TargetElement): HTMLElement;
addEventCallback(eventName: EventType, callback: EventCallback): void;
modifyRevertSubmissionMsg(
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts
index 46c7ad6..2f9bbcf 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.ts
@@ -22,7 +22,7 @@
import {windowLocationReload} from '../../../utils/dom-util';
import {PopupPluginApi} from '../../../api/popup';
import {GrPopupInterface} from '../../plugins/gr-popup-interface/gr-popup-interface';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
interface ButtonCallBacks {
onclick: (event: Event) => boolean;
@@ -31,7 +31,7 @@
export class GrPluginActionContext {
private popups: PopupPluginApi[] = [];
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
constructor(
public readonly plugin: PluginApi,
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
index 34c976a..d4b93a7 100644
--- 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
@@ -18,18 +18,15 @@
import '../../../test/common-test-setup-karma.js';
import './gr-js-api-interface.js';
import {GrPluginActionContext} from './gr-plugin-action-context.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
import {addListenerForTest} from '../../../test/test-utils.js';
-const pluginApi = _testOnly_initGerritPluginApi();
-
suite('gr-plugin-action-context tests', () => {
let instance;
let plugin;
setup(() => {
- pluginApi.install(p => { plugin = p; }, '0.1',
+ window.Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
instance = new GrPluginActionContext(plugin);
});
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 b039a7e..38b4aee 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
@@ -177,7 +177,6 @@
}
}
-// TODO(dmfilippov): Convert to service and add to appContext
let pluginEndpoints = new GrPluginEndpoints();
// To avoid mutable-exports, we don't want to export above variable directly
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 c7bdfb4..16846f4 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
@@ -18,12 +18,9 @@
import {resetPlugins} from '../../../test/test-utils';
import './gr-js-api-interface';
import {GrPluginEndpoints} from './gr-plugin-endpoints';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit';
import {PluginApi} from '../../../api/plugin';
import {HookApi, HookCallback, PluginElement} from '../../../api/hook';
-const pluginApi = _testOnly_initGerritPluginApi();
-
export class MockHook<T extends PluginElement> implements HookApi<T> {
handleInstanceDetached(_: T) {}
@@ -59,7 +56,7 @@
setup(() => {
domHook = new MockHook<PluginElement>();
instance = new GrPluginEndpoints();
- pluginApi.install(
+ window.Gerrit.install(
plugin => (decoratePlugin = plugin),
'0.1',
'http://test.com/plugins/testplugin/static/decorate.js'
@@ -70,7 +67,7 @@
moduleName: 'decorate-module',
domHook,
});
- pluginApi.install(
+ window.Gerrit.install(
plugin => (stylePlugin = plugin),
'0.1',
'http://test.com/plugins/testplugin/static/style.js'
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 7c99480..bef5b97 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
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {PLUGIN_LOADING_TIMEOUT_MS, getPluginNameFromUrl} from './gr-api-utils';
import {Plugin} from './gr-public-js-api';
import {getBaseUrl} from '../../../utils/url-util';
@@ -22,6 +22,7 @@
import {PluginApi} from '../../../api/plugin';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
import {ShowAlertEventDetail} from '../../../types/events';
+import {fireAlert} from '../../../utils/event-util';
enum PluginState {
/** State that indicates the plugin is pending to be loaded. */
@@ -85,7 +86,7 @@
_getReporting() {
if (!this._reporting) {
- this._reporting = appContext.reportingService;
+ this._reporting = getAppContext().reportingService;
}
return this._reporting;
}
@@ -134,8 +135,8 @@
if (!(url instanceof URL)) {
try {
url = new URL(url);
- } catch (e) {
- this._getReporting().error(e);
+ } catch (e: unknown) {
+ this._getReporting().error(new Error('url parse error'), undefined, e);
return false;
}
}
@@ -182,8 +183,16 @@
try {
callback(plugin);
this._pluginInstalled(url, plugin);
- } catch (e) {
- this._failToLoad(`${e.name}: ${e.message}`, src);
+ } catch (e: unknown) {
+ if (e instanceof Error) {
+ this._failToLoad(`${e.name}: ${e.message}`, src);
+ } else {
+ this._getReporting().error(
+ new Error('plugin callback error'),
+ undefined,
+ e
+ );
+ }
}
}
@@ -209,9 +218,11 @@
this._updatePluginState(plugin.url, PluginState.LOAD_FAILED);
}
this._checkIfCompleted();
- return `Timeout when loading plugins: ${pending
+ const errorMessage = `Timeout when loading plugins: ${pending
.map(p => p.name)
.join(',')}`;
+ fireAlert(document, errorMessage);
+ return errorMessage;
}
_failToLoad(message: string, pluginUrl?: string) {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js
index ab69267..e097858 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js
@@ -19,11 +19,8 @@
import {PLUGIN_LOADING_TIMEOUT_MS} from './gr-api-utils.js';
import {_testOnly_resetPluginLoader} from './gr-plugin-loader.js';
import {resetPlugins, stubBaseUrl} from '../../../test/test-utils.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
import {addListenerForTest, stubRestApi} from '../../../test/test-utils.js';
-const pluginApi = _testOnly_initGerritPluginApi();
-
suite('gr-plugin-loader tests', () => {
let plugin;
@@ -47,18 +44,18 @@
});
test('reuse plugin for install calls', () => {
- pluginApi.install(p => { plugin = p; }, '0.1',
+ window.Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
let otherPlugin;
- pluginApi.install(p => { otherPlugin = p; }, '0.1',
+ window.Gerrit.install(p => { otherPlugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
assert.strictEqual(plugin, otherPlugin);
});
test('versioning', () => {
const callback = sinon.spy();
- pluginApi.install(callback, '0.0pre-alpha');
+ window.Gerrit.install(callback, '0.0pre-alpha');
assert(callback.notCalled);
});
@@ -89,7 +86,7 @@
test('plugins installed successfully', async () => {
sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
- pluginApi.install(() => void 0, undefined, url);
+ window.Gerrit.install(() => void 0, undefined, url);
});
const pluginsLoadedStub = sinon.stub(pluginLoader._getReporting(),
'pluginsLoaded');
@@ -107,7 +104,7 @@
test('isPluginEnabled and isPluginLoaded', () => {
sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
- pluginApi.install(() => void 0, undefined, url);
+ window.Gerrit.install(() => void 0, undefined, url);
});
const plugins = [
@@ -137,7 +134,7 @@
addListenerForTest(document, 'show-alert', alertStub);
sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
- pluginApi.install(() => {
+ window.Gerrit.install(() => {
if (url === plugins[0]) {
throw new Error('failed');
}
@@ -165,7 +162,7 @@
addListenerForTest(document, 'show-alert', alertStub);
sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
- pluginApi.install(() => {
+ window.Gerrit.install(() => {
if (url === plugins[0]) {
throw new Error('failed');
}
@@ -198,7 +195,7 @@
addListenerForTest(document, 'show-alert', alertStub);
sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
- pluginApi.install(() => {
+ window.Gerrit.install(() => {
throw new Error('failed');
}, undefined, url);
});
@@ -224,7 +221,7 @@
addListenerForTest(document, 'show-alert', alertStub);
sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
- pluginApi.install(() => {
+ window.Gerrit.install(() => {
}, url === plugins[0] ? '' : 'alpha', url);
});
@@ -241,7 +238,7 @@
test('multiple assets for same plugin installed successfully', async () => {
sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
- pluginApi.install(() => void 0, undefined, url);
+ window.Gerrit.install(() => void 0, undefined, url);
});
const pluginsLoadedStub = sinon.stub(pluginLoader._getReporting(),
'pluginsLoaded');
@@ -388,7 +385,7 @@
}
}
sinon.stub(pluginLoader, '_loadJsPlugin').callsFake( url => {
- pluginApi.install(() => pluginCallback(url), undefined, url);
+ window.Gerrit.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 2b6db21..3c2842d 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
@@ -16,7 +16,7 @@
*/
import {HttpMethod} from '../../../constants/constants';
import {RequestPayload} from '../../../types/common';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {ErrorCallback, RestPluginApi} from '../../../api/rest';
import {PluginApi} from '../../../api/plugin';
@@ -35,9 +35,9 @@
}
export class GrPluginRestApi implements RestPluginApi {
- private readonly restApi = appContext.restApiService;
+ private readonly restApi = getAppContext().restApiService;
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
constructor(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.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.js
index d2b5658..730f163 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.js
@@ -18,11 +18,8 @@
import '../../../test/common-test-setup-karma.js';
import './gr-js-api-interface.js';
import {GrPluginRestApi} from './gr-plugin-rest-api.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
import {stubRestApi} from '../../../test/test-utils.js';
-const pluginApi = _testOnly_initGerritPluginApi();
-
suite('gr-plugin-rest-api tests', () => {
let instance;
let getResponseObjectStub;
@@ -33,7 +30,7 @@
getResponseObjectStub = stubRestApi('getResponseObject').returns(
Promise.resolve());
sendStub = stubRestApi('send').returns(Promise.resolve({status: 200}));
- pluginApi.install(p => {}, '0.1',
+ window.Gerrit.install(p => {}, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
instance = new GrPluginRestApi();
});
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 18737a9..30f91b8 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
@@ -32,7 +32,7 @@
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 {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {AdminPluginApi} from '../../../api/admin';
import {AnnotationPluginApi} from '../../../api/annotation';
import {EventHelperPluginApi} from '../../../api/event-helper';
@@ -71,9 +71,11 @@
private readonly _name: string = PLUGIN_NAME_NOT_SET;
- private readonly jsApi = appContext.jsApiService;
+ private readonly jsApi = getAppContext().jsApiService;
- private readonly report = appContext.reportingService;
+ private readonly report = getAppContext().reportingService;
+
+ private readonly restApiService = getAppContext().restApiService;
constructor(url?: string) {
this.domHooks = new GrDomHooksManager(this);
@@ -175,7 +177,7 @@
getServerInfo() {
this.report.trackApi(this, 'plugin', 'getServerInfo');
- return appContext.restApiService.getConfig();
+ return this.restApiService.getConfig();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -212,7 +214,7 @@
callback?: SendCallback,
payload?: RequestPayload
) {
- return send(method, this.url(url), callback, payload);
+ return send(this.restApiService, method, this.url(url), callback, payload);
}
annotationApi(): AnnotationPluginApi {
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 0427b43..d600101 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
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {PluginApi} from '../../../api/plugin';
import {EventDetails, ReportingPluginApi} from '../../../api/reporting';
@@ -23,7 +23,7 @@
* Defines all methods that will be exported to plugin from reporting service.
*/
export class GrReportingJsApi implements ReportingPluginApi {
- private readonly reporting = appContext.reportingService;
+ private readonly reporting = getAppContext().reportingService;
constructor(private readonly plugin: PluginApi) {
this.reporting.trackApi(this.plugin, 'reporting', 'constructor');
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api_test.js
deleted file mode 100644
index 71cc565..0000000
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api_test.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import '../../change/gr-reply-dialog/gr-reply-dialog.js';
-import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
-import {appContext} from '../../../services/app-context.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-
-const pluginApi = _testOnly_initGerritPluginApi();
-
-suite('gr-reporting-js-api tests', () => {
- let reporting;
- let plugin;
-
- setup(() => {
- stubRestApi('getAccount').returns(Promise.resolve(null));
- });
-
- suite('early init', () => {
- setup(() => {
- pluginApi.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- reporting = plugin.reporting();
- });
-
- teardown(() => {
- reporting = null;
- });
-
- test('redirect reportInteraction call to reportingService', () => {
- sinon.spy(appContext.reportingService, 'reportPluginInteractionLog');
- reporting.reportInteraction('test', {});
- assert.isTrue(appContext.reportingService.reportPluginInteractionLog
- .called);
- assert.equal(
- appContext.reportingService.reportPluginInteractionLog.lastCall
- .args[0],
- 'testplugin-test'
- );
- assert.deepEqual(
- appContext.reportingService.reportPluginInteractionLog.lastCall
- .args[1],
- {}
- );
- });
-
- test('redirect reportLifeCycle call to reportingService', () => {
- sinon.spy(appContext.reportingService, 'reportPluginLifeCycleLog');
- reporting.reportLifeCycle('test', {});
- assert.isTrue(appContext.reportingService.reportPluginLifeCycleLog
- .called);
- assert.equal(
- appContext.reportingService.reportPluginLifeCycleLog.lastCall.args[0],
- 'testplugin-test'
- );
- assert.deepEqual(
- appContext.reportingService.reportPluginLifeCycleLog.lastCall.args[1],
- {}
- );
- });
- });
-});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api_test.ts
new file mode 100644
index 0000000..c96a075
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api_test.ts
@@ -0,0 +1,64 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma.js';
+import '../../change/gr-reply-dialog/gr-reply-dialog.js';
+import {getAppContext} from '../../../services/app-context.js';
+import {stubRestApi} from '../../../test/test-utils.js';
+import {PluginApi} from '../../../api/plugin.js';
+import {ReportingService} from '../../../services/gr-reporting/gr-reporting.js';
+import {ReportingPluginApi} from '../../../api/reporting.js';
+
+suite('gr-reporting-js-api tests', () => {
+ let plugin: PluginApi;
+ let reportingService: ReportingService;
+
+ setup(() => {
+ stubRestApi('getAccount').returns(Promise.resolve(undefined));
+ reportingService = getAppContext().reportingService;
+ });
+
+ suite('early init', () => {
+ let reporting: ReportingPluginApi;
+ setup(() => {
+ window.Gerrit.install(
+ p => {
+ plugin = p;
+ },
+ '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js'
+ );
+ reporting = plugin.reporting();
+ });
+
+ test('redirect reportInteraction call to reportingService', () => {
+ const spy = sinon.spy(reportingService, 'reportPluginInteractionLog');
+ reporting.reportInteraction('test', {});
+ assert.isTrue(spy.called);
+ assert.equal(spy.lastCall.args[0], 'testplugin-test');
+ assert.deepEqual(spy.lastCall.args[1], {});
+ });
+
+ test('redirect reportLifeCycle call to reportingService', () => {
+ const spy = sinon.spy(reportingService, 'reportPluginLifeCycleLog');
+ reporting.reportLifeCycle('test', {});
+ assert.isTrue(spy.called);
+ assert.equal(spy.lastCall.args[0], 'testplugin-test');
+ assert.deepEqual(spy.lastCall.args[1], {});
+ });
+ });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
index a65bb75..a0b3274 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.ts
@@ -44,16 +44,16 @@
getVotingRangeOrDefault,
hasNeutralStatus,
hasVoted,
+ showNewSubmitRequirements,
valueString,
} from '../../../utils/label-util';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {ParsedChangeInfo} from '../../../types/types';
import {fontStyles} from '../../../styles/gr-font-styles';
import {sharedStyles} from '../../../styles/shared-styles';
import {votingStyles} from '../../../styles/gr-voting-styles';
import {ifDefined} from 'lit/directives/if-defined';
import {fireReload} from '../../../utils/event-util';
-import {KnownExperimentId} from '../../../services/flags/flags';
import {sortReviewers} from '../../../utils/attention-set-util';
declare global {
@@ -104,13 +104,11 @@
@property({type: Boolean})
showAllReviewers = true;
- /** temporary until submit requirements are finished */
- @property({type: Boolean})
- showAlwaysOldUI = false;
+ private readonly restApiService = getAppContext().restApiService;
- private readonly restApiService = appContext.restApiService;
+ private readonly reporting = getAppContext().reportingService;
- private readonly reporting = appContext.reportingService;
+ private readonly flagsService = getAppContext().flagsService;
// TODO(TS): not used, remove later
_xhrPromise?: Promise<void>;
@@ -211,13 +209,8 @@
];
}
- private readonly flagsService = appContext.flagsService;
-
override render() {
- if (
- this.flagsService.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI) &&
- !this.showAlwaysOldUI
- ) {
+ if (showNewSubmitRequirements(this.flagsService, this.change)) {
return this.renderNewSubmitRequirements();
} else {
return this.renderOldSubmitRequirements();
@@ -318,7 +311,9 @@
link
aria-label="Remove vote"
@click="${this.onDeleteVote}"
- data-account-id="${ifDefined(reviewer._account_id)}"
+ data-account-id="${ifDefined(
+ reviewer._account_id as number | undefined
+ )}"
class="deleteBtn ${this.computeDeleteClass(
reviewer,
this.mutable,
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.ts b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.ts
index a3f128b..fe69a32 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.ts
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.ts
@@ -86,7 +86,7 @@
reject(new Error('Unable to load blank script url.'));
return;
}
-
+ script.setAttribute('crossorigin', 'anonymous');
script.setAttribute('src', src);
script.onload = resolve;
script.onerror = reject;
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/highlightjs_config.ts b/polygerrit-ui/app/elements/shared/gr-lib-loader/highlightjs_config.ts
index da13396..f63779c 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/highlightjs_config.ts
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/highlightjs_config.ts
@@ -17,7 +17,7 @@
import '../gr-js-api-interface/gr-js-api-interface';
import {EventType} from '../../../api/plugin';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {LibraryConfig} from './gr-lib-loader';
@@ -27,7 +27,7 @@
checkPresent: () => window.hljs !== undefined,
configureCallback: () => {
window.hljs!.configure({classPrefix: 'gr-diff gr-syntax gr-syntax-'});
- appContext.jsApiService.handleEvent(EventType.HIGHLIGHTJS_LOADED, {
+ getAppContext().jsApiService.handleEvent(EventType.HIGHLIGHTJS_LOADED, {
hljs: window.hljs,
});
return window.hljs;
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.ts b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.ts
index 7008db2..9bb112e 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.ts
@@ -16,6 +16,7 @@
*/
import {customElement, property} from 'lit/decorators';
import {html, LitElement} from 'lit';
+import '../gr-tooltip-content/gr-tooltip-content';
declare global {
interface HTMLElementTagNameMap {
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts
index 4f02897..ab5f1ad 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts
@@ -16,15 +16,15 @@
*/
import '@polymer/iron-input/iron-input';
import '@polymer/iron-icon/iron-icon';
-import '../../../styles/shared-styles';
import '../gr-button/gr-button';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-list-view_html';
import {encodeURL, getBaseUrl} from '../../../utils/url-util';
import {page} from '../../../utils/page-wrapper-utils';
-import {property, customElement} from '@polymer/decorators';
import {fireEvent} from '../../../utils/event-util';
import {debounce, DelayedTask} from '../../../utils/async-util';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, PropertyValues, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
+import {BindValueChangeEvent} from '../../../types/events';
const REQUEST_DEBOUNCE_INTERVAL_MS = 200;
@@ -35,11 +35,7 @@
}
@customElement('gr-list-view')
-export class GrListView extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrListView extends LitElement {
@property({type: Boolean})
createNew?: boolean;
@@ -49,7 +45,7 @@
@property({type: Number})
itemsPerPage = 25;
- @property({type: String, observer: '_filterChanged'})
+ @property({type: String})
filter?: string;
@property({type: Number})
@@ -68,37 +64,148 @@
super.disconnectedCallback();
}
- _filterChanged(newFilter?: string, oldFilter?: string) {
+ static override get styles() {
+ return [
+ sharedStyles,
+ css`
+ #filter {
+ max-width: 25em;
+ }
+ #filter:focus {
+ outline: none;
+ }
+ #topContainer {
+ align-items: center;
+ display: flex;
+ height: 3rem;
+ justify-content: space-between;
+ margin: 0 var(--spacing-l);
+ }
+ #createNewContainer:not(.show) {
+ display: none;
+ }
+ a {
+ color: var(--primary-text-color);
+ text-decoration: none;
+ }
+ a:hover {
+ text-decoration: underline;
+ }
+ nav {
+ align-items: center;
+ display: flex;
+ height: 3rem;
+ justify-content: flex-end;
+ margin-right: 20px;
+ }
+ nav,
+ iron-icon {
+ color: var(--deemphasized-text-color);
+ }
+ iron-icon {
+ height: 1.85rem;
+ margin-left: 16px;
+ width: 1.85rem;
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`
+ <div id="topContainer">
+ <div class="filterContainer">
+ <label>Filter:</label>
+ <iron-input
+ .bindValue=${this.filter}
+ @bind-value-changed=${this.handleFilterBindValueChanged}
+ >
+ <input type="text" id="filter" />
+ </iron-input>
+ </div>
+ <div id="createNewContainer" class=${this.createNew ? 'show' : ''}>
+ <gr-button
+ id="createNew"
+ primary
+ link
+ @click=${() => this.createNewItem()}
+ >
+ Create New
+ </gr-button>
+ </div>
+ </div>
+ <slot></slot>
+ <nav>
+ Page ${this.computePage(this.offset, this.itemsPerPage)}
+ <a
+ id="prevArrow"
+ href=${this.computeNavLink(
+ this.offset,
+ -1,
+ this.itemsPerPage,
+ this.filter,
+ this.path
+ )}
+ ?hidden=${this.loading || this.offset === 0}
+ >
+ <iron-icon icon="gr-icons:chevron-left"></iron-icon>
+ </a>
+ <a
+ id="nextArrow"
+ href=${this.computeNavLink(
+ this.offset,
+ 1,
+ this.itemsPerPage,
+ this.filter,
+ this.path
+ )}
+ ?hidden=${this.hideNextArrow(this.loading, this.items)}
+ >
+ <iron-icon icon="gr-icons:chevron-right"></iron-icon>
+ </a>
+ </nav>
+ `;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ // We have to do this for the tests.
+ if (changedProperties.has('filter')) {
+ this.filterChanged(
+ this.filter,
+ changedProperties.get('filter') as string
+ );
+ }
+ }
+
+ private filterChanged(newFilter?: string, oldFilter?: string) {
// newFilter can be empty string and then !newFilter === true
if (!newFilter && !oldFilter) {
return;
}
-
- this._debounceReload(newFilter);
+ this.debounceReload(newFilter);
}
- _debounceReload(filter?: string) {
+ // private but used in test
+ debounceReload(filter?: string) {
this.reloadTask = debounce(
this.reloadTask,
() => {
- if (this.path) {
- if (filter) {
- return page.show(
- `${this.path}/q/filter:` + encodeURL(filter, false)
- );
- }
- return page.show(this.path);
+ if (!this.isConnected || !this.path) return;
+ if (filter) {
+ return page.show(`${this.path}/q/filter:${encodeURL(filter, false)}`);
}
+ return page.show(this.path);
},
REQUEST_DEBOUNCE_INTERVAL_MS
);
}
- _createNewItem() {
+ private createNewItem() {
fireEvent(this, 'create-clicked');
}
- _computeNavLink(
+ // private but used in test
+ computeNavLink(
offset: number,
direction: number,
itemsPerPage: number,
@@ -118,15 +225,8 @@
return href;
}
- _computeCreateClass(createNew?: boolean) {
- return createNew ? 'show' : '';
- }
-
- _hidePrevArrow(loading?: boolean, offset?: number) {
- return loading || offset === 0;
- }
-
- _hideNextArrow(loading?: boolean, items?: unknown[]) {
+ // private but used in test
+ hideNextArrow(loading?: boolean, items?: unknown[]) {
if (loading || !items || !items.length) {
return true;
}
@@ -137,7 +237,12 @@
// TODO: fix offset (including itemsPerPage)
// to either support a decimal or make it go to the nearest
// whole number (e.g 3).
- _computePage(offset: number, itemsPerPage: number) {
+ // private but used in test
+ computePage(offset: number, itemsPerPage: number) {
return offset / itemsPerPage + 1;
}
+
+ private handleFilterBindValueChanged(e: BindValueChangeEvent) {
+ this.filter = e.detail.value;
+ }
}
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_html.ts b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_html.ts
deleted file mode 100644
index 75ee667..0000000
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_html.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- #filter {
- max-width: 25em;
- }
- #filter:focus {
- outline: none;
- }
- #topContainer {
- align-items: center;
- display: flex;
- height: 3rem;
- justify-content: space-between;
- margin: 0 var(--spacing-l);
- }
- #createNewContainer:not(.show) {
- display: none;
- }
- a {
- color: var(--primary-text-color);
- text-decoration: none;
- }
- a:hover {
- text-decoration: underline;
- }
- nav {
- align-items: center;
- display: flex;
- height: 3rem;
- justify-content: flex-end;
- margin-right: 20px;
- }
- nav,
- iron-icon {
- color: var(--deemphasized-text-color);
- }
- iron-icon {
- height: 1.85rem;
- margin-left: 16px;
- width: 1.85rem;
- }
- </style>
- <div id="topContainer">
- <div class="filterContainer">
- <label>Filter:</label>
- <iron-input type="text" bind-value="{{filter}}">
- <input
- is="iron-input"
- type="text"
- id="filter"
- bind-value="{{filter}}"
- />
- </iron-input>
- </div>
- <div id="createNewContainer" class$="[[_computeCreateClass(createNew)]]">
- <gr-button primary="" link="" id="createNew" on-click="_createNewItem">
- Create New
- </gr-button>
- </div>
- </div>
- <slot></slot>
- <nav>
- Page [[_computePage(offset, itemsPerPage)]]
- <a
- id="prevArrow"
- href$="[[_computeNavLink(offset, -1, itemsPerPage, filter, path)]]"
- hidden$="[[_hidePrevArrow(loading, offset)]]"
- hidden=""
- >
- <iron-icon icon="gr-icons:chevron-left"></iron-icon>
- </a>
- <a
- id="nextArrow"
- href$="[[_computeNavLink(offset, 1, itemsPerPage, filter, path)]]"
- hidden$="[[_hideNextArrow(loading, items)]]"
- hidden=""
- >
- <iron-icon icon="gr-icons:chevron-right"></iron-icon>
- </a>
- </nav>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.js
deleted file mode 100644
index 066c53e..0000000
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.js
+++ /dev/null
@@ -1,147 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-list-view.js';
-import {page} from '../../../utils/page-wrapper-utils.js';
-import {stubBaseUrl} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-list-view');
-
-suite('gr-list-view tests', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- });
-
- test('_computeNavLink', () => {
- const offset = 25;
- const projectsPerPage = 25;
- let filter = 'test';
- const path = '/admin/projects';
-
- stubBaseUrl('');
-
- assert.equal(
- element._computeNavLink(offset, 1, projectsPerPage, filter, path),
- '/admin/projects/q/filter:test,50');
-
- assert.equal(
- element._computeNavLink(offset, -1, projectsPerPage, filter, path),
- '/admin/projects/q/filter:test');
-
- assert.equal(
- element._computeNavLink(offset, 1, projectsPerPage, null, path),
- '/admin/projects,50');
-
- assert.equal(
- element._computeNavLink(offset, -1, projectsPerPage, null, path),
- '/admin/projects');
-
- filter = 'plugins/';
- assert.equal(
- element._computeNavLink(offset, 1, projectsPerPage, filter, path),
- '/admin/projects/q/filter:plugins%252F,50');
- });
-
- test('_onValueChange', async () => {
- let resolve;
- const promise = new Promise(r => resolve = r);
- element.path = '/admin/projects';
- sinon.stub(page, 'show').callsFake(resolve);
-
- element.filter = 'test';
-
- const url = await promise;
- assert.equal(url, '/admin/projects/q/filter:test');
- });
-
- test('_filterChanged not reload when swap between falsy values', () => {
- sinon.stub(element, '_debounceReload');
- element.filter = null;
- element.filter = undefined;
- element.filter = '';
- assert.isFalse(element._debounceReload.called);
- });
-
- test('next button', () => {
- element.itemsPerPage = 25;
- let projects = new Array(26);
- flush();
-
- let loading;
- assert.isFalse(element._hideNextArrow(loading, projects));
- loading = true;
- assert.isTrue(element._hideNextArrow(loading, projects));
- loading = false;
- assert.isFalse(element._hideNextArrow(loading, projects));
- element._projects = [];
- assert.isTrue(element._hideNextArrow(loading, element._projects));
- projects = new Array(4);
- assert.isTrue(element._hideNextArrow(loading, projects));
- });
-
- test('prev button', () => {
- assert.isTrue(element._hidePrevArrow(true, 0));
- flush(() => {
- let offset = 0;
- assert.isTrue(element._hidePrevArrow(false, offset));
- offset = 5;
- assert.isFalse(element._hidePrevArrow(false, offset));
- });
- });
-
- test('createNew link appears correctly', () => {
- assert.isFalse(element.shadowRoot
- .querySelector('#createNewContainer').classList
- .contains('show'));
- element.createNew = true;
- flush();
- assert.isTrue(element.shadowRoot
- .querySelector('#createNewContainer').classList
- .contains('show'));
- });
-
- test('fires create clicked event when button tapped', () => {
- const clickHandler = sinon.stub();
- element.addEventListener('create-clicked', clickHandler);
- element.createNew = true;
- flush();
- MockInteractions.tap(element.shadowRoot.querySelector('#createNew'));
- assert.isTrue(clickHandler.called);
- });
-
- test('next/prev links change when path changes', () => {
- const BRANCHES_PATH = '/path/to/branches';
- const TAGS_PATH = '/path/to/tags';
- sinon.stub(element, '_computeNavLink');
- element.offset = 0;
- element.itemsPerPage = 25;
- element.filter = '';
- element.path = BRANCHES_PATH;
- assert.equal(element._computeNavLink.lastCall.args[4], BRANCHES_PATH);
- element.path = TAGS_PATH;
- assert.equal(element._computeNavLink.lastCall.args[4], TAGS_PATH);
- });
-
- test('_computePage', () => {
- assert.equal(element._computePage(0, 25), 1);
- assert.equal(element._computePage(50, 25), 3);
- });
-});
-
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.ts b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.ts
new file mode 100644
index 0000000..29cbb1a
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.ts
@@ -0,0 +1,182 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-list-view';
+import {GrListView} from './gr-list-view';
+import {page} from '../../../utils/page-wrapper-utils';
+import {queryAndAssert, stubBaseUrl} from '../../../test/test-utils';
+import {GrButton} from '../gr-button/gr-button';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+
+const basicFixture = fixtureFromElement('gr-list-view');
+
+suite('gr-list-view tests', () => {
+ let element: GrListView;
+
+ setup(async () => {
+ element = basicFixture.instantiate();
+ await element.updateComplete;
+ });
+
+ test('computeNavLink', () => {
+ const offset = 25;
+ const projectsPerPage = 25;
+ let filter = 'test';
+ const path = '/admin/projects';
+
+ stubBaseUrl('');
+
+ assert.equal(
+ element.computeNavLink(offset, 1, projectsPerPage, filter, path),
+ '/admin/projects/q/filter:test,50'
+ );
+
+ assert.equal(
+ element.computeNavLink(offset, -1, projectsPerPage, filter, path),
+ '/admin/projects/q/filter:test'
+ );
+
+ assert.equal(
+ element.computeNavLink(offset, 1, projectsPerPage, undefined, path),
+ '/admin/projects,50'
+ );
+
+ assert.equal(
+ element.computeNavLink(offset, -1, projectsPerPage, undefined, path),
+ '/admin/projects'
+ );
+
+ filter = 'plugins/';
+ assert.equal(
+ element.computeNavLink(offset, 1, projectsPerPage, filter, path),
+ '/admin/projects/q/filter:plugins%252F,50'
+ );
+ });
+
+ test('_onValueChange', async () => {
+ let resolve: (url: string) => void;
+ const promise = new Promise(r => (resolve = r));
+ element.path = '/admin/projects';
+ sinon.stub(page, 'show').callsFake(r => resolve(r));
+
+ element.filter = 'test';
+ await element.updateComplete;
+
+ const url = await promise;
+ assert.equal(url, '/admin/projects/q/filter:test');
+ });
+
+ test('_filterChanged not reload when swap between falsy values', () => {
+ const debounceReloadStub = sinon.stub(element, 'debounceReload');
+ element.filter = undefined;
+ element.filter = '';
+ assert.isFalse(debounceReloadStub.called);
+ });
+
+ test('next button', async () => {
+ element.itemsPerPage = 25;
+ let projects = new Array(26);
+ await element.updateComplete;
+
+ let loading;
+ assert.isFalse(element.hideNextArrow(loading, projects));
+ loading = true;
+ assert.isTrue(element.hideNextArrow(loading, projects));
+ loading = false;
+ assert.isFalse(element.hideNextArrow(loading, projects));
+ projects = [];
+ assert.isTrue(element.hideNextArrow(loading, projects));
+ projects = new Array(4);
+ assert.isTrue(element.hideNextArrow(loading, projects));
+ });
+
+ test('prev button', async () => {
+ element.loading = true;
+ element.offset = 0;
+ await element.updateComplete;
+ assert.isTrue(
+ queryAndAssert<HTMLAnchorElement>(element, '#prevArrow').hasAttribute(
+ 'hidden'
+ )
+ );
+
+ element.loading = false;
+ element.offset = 0;
+ await element.updateComplete;
+ assert.isTrue(
+ queryAndAssert<HTMLAnchorElement>(element, '#prevArrow').hasAttribute(
+ 'hidden'
+ )
+ );
+
+ element.loading = false;
+ element.offset = 5;
+ await element.updateComplete;
+ assert.isFalse(
+ queryAndAssert<HTMLAnchorElement>(element, '#prevArrow').hasAttribute(
+ 'hidden'
+ )
+ );
+ });
+
+ test('createNew link appears correctly', async () => {
+ assert.isFalse(
+ queryAndAssert<HTMLDivElement>(
+ element,
+ '#createNewContainer'
+ ).classList.contains('show')
+ );
+ element.createNew = true;
+ await element.updateComplete;
+ assert.isTrue(
+ queryAndAssert<HTMLDivElement>(
+ element,
+ '#createNewContainer'
+ ).classList.contains('show')
+ );
+ });
+
+ test('fires create clicked event when button tapped', async () => {
+ const clickHandler = sinon.stub();
+ element.addEventListener('create-clicked', clickHandler);
+ element.createNew = true;
+ await element.updateComplete;
+ MockInteractions.tap(queryAndAssert<GrButton>(element, '#createNew'));
+ assert.isTrue(clickHandler.called);
+ });
+
+ test('next/prev links change when path changes', async () => {
+ const BRANCHES_PATH = '/path/to/branches';
+ const TAGS_PATH = '/path/to/tags';
+ const computeNavLinkStub = sinon.stub(element, 'computeNavLink');
+ element.offset = 0;
+ element.itemsPerPage = 25;
+ element.filter = '';
+ element.path = BRANCHES_PATH;
+ await element.updateComplete;
+ assert.equal(computeNavLinkStub.lastCall.args[4], BRANCHES_PATH);
+ element.path = TAGS_PATH;
+ await element.updateComplete;
+ assert.equal(computeNavLinkStub.lastCall.args[4], TAGS_PATH);
+ });
+
+ test('computePage', () => {
+ assert.equal(element.computePage(0, 25), 1);
+ assert.equal(element.computePage(50, 25), 3);
+ });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.ts b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.ts
index 423a1a8..211f6dc 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.ts
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.ts
@@ -14,22 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-page-nav_html';
-import {customElement, property} from '@polymer/decorators';
-/**
- * Augment the interface on top of PolymerElement
- * for gr-page-nav.
- */
-export interface GrPageNav {
- $: {
- // Note: this is needed to access $.nav
- // with dotted property access
- nav: HTMLElement;
- };
-}
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, css, html} from 'lit';
+import {customElement, query, state} from 'lit/decorators';
+import {assertIsDefined} from '../../../utils/common-util';
declare global {
interface HTMLElementTagNameMap {
@@ -38,19 +27,17 @@
}
@customElement('gr-page-nav')
-export class GrPageNav extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
+export class GrPageNav extends LitElement {
+ @query('nav') private nav?: HTMLElement;
- @property({type: Number})
- _headerHeight?: number;
+ // private but used in test
+ @state() headerHeight?: number;
private readonly bodyScrollHandler: () => void;
constructor() {
super();
- this.bodyScrollHandler = () => this._handleBodyScroll();
+ this.bodyScrollHandler = () => this.handleBodyScroll();
}
override connectedCallback() {
@@ -63,40 +50,77 @@
super.disconnectedCallback();
}
- _handleBodyScroll() {
- if (this._headerHeight === undefined) {
- let top = this._getOffsetTop(this);
+ static override get styles() {
+ return [
+ sharedStyles,
+ css`
+ nav {
+ background-color: var(--table-header-background-color);
+ border: 1px solid var(--border-color);
+ border-top: none;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ width: 14em;
+ }
+ nav.pinned {
+ position: fixed;
+ }
+ @media only screen and (max-width: 53em) {
+ nav {
+ display: none;
+ }
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`
+ <nav aria-label="Sidebar">
+ <slot></slot>
+ </nav>
+ `;
+ }
+
+ // private but used in test
+ handleBodyScroll() {
+ assertIsDefined(this.nav, 'nav');
+ if (this.headerHeight === undefined) {
+ let top = this.getOffsetTop(this);
// TODO(TS): Element doesn't have offsetParent,
// while `offsetParent` are returning Element not HTMLElement
for (
let offsetParent = this.offsetParent as HTMLElement | undefined;
offsetParent;
- offsetParent = this._getOffsetParent(offsetParent)
+ offsetParent = this.getOffsetParent(offsetParent)
) {
- top += this._getOffsetTop(offsetParent);
+ top += this.getOffsetTop(offsetParent);
}
- this._headerHeight = top;
+ this.headerHeight = top;
}
- this.$.nav.classList.toggle(
+ this.nav.classList.toggle(
'pinned',
- this._getScrollY() >= (this._headerHeight || 0)
+ this.getScrollY() >= (this.headerHeight || 0)
);
}
/* Functions used for test purposes */
- _getOffsetParent(element?: HTMLElement) {
+ private getOffsetParent(element?: HTMLElement) {
if (!element || !('offsetParent' in element)) {
return undefined;
}
return element.offsetParent as HTMLElement;
}
- _getOffsetTop(element: HTMLElement) {
+ // private but used in test
+ getOffsetTop(element: HTMLElement) {
return element.offsetTop;
}
- _getScrollY() {
+ // private but used in test
+ getScrollY() {
return window.scrollY;
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_html.ts b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_html.ts
deleted file mode 100644
index a9d9216..0000000
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_html.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- #nav {
- background-color: var(--table-header-background-color);
- border: 1px solid var(--border-color);
- border-top: none;
- height: 100%;
- position: absolute;
- top: 0;
- width: 14em;
- }
- #nav.pinned {
- position: fixed;
- }
- @media only screen and (max-width: 53em) {
- #nav {
- display: none;
- }
- }
- </style>
- <nav id="nav" aria-label="Sidebar">
- <slot></slot>
- </nav>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.js b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.js
deleted file mode 100644
index 2960a1f..0000000
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.js
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-page-nav.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-
-const basicFixture = fixtureFromTemplate(html`
-<gr-page-nav>
- <ul>
- <li>item</li>
- </ul>
- </gr-page-nav>
-`);
-
-suite('gr-page-nav tests', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- flush();
- });
-
- test('header is not pinned just below top', () => {
- sinon.stub(element, '_getOffsetParent').callsFake(() => 0);
- sinon.stub(element, '_getOffsetTop').callsFake(() => 10);
- sinon.stub(element, '_getScrollY').callsFake(() => 5);
- element._handleBodyScroll();
- assert.isFalse(element.$.nav.classList.contains('pinned'));
- });
-
- test('header is pinned when scroll down the page', () => {
- sinon.stub(element, '_getOffsetParent').callsFake(() => 0);
- sinon.stub(element, '_getOffsetTop').callsFake(() => 10);
- sinon.stub(element, '_getScrollY').callsFake(() => 25);
- window.scrollY = 100;
- element._handleBodyScroll();
- assert.isTrue(element.$.nav.classList.contains('pinned'));
- });
-
- test('header is not pinned just below top with header set', () => {
- element._headerHeight = 20;
- sinon.stub(element, '_getScrollY').callsFake(() => 15);
- window.scrollY = 100;
- element._handleBodyScroll();
- assert.isFalse(element.$.nav.classList.contains('pinned'));
- });
-
- test('header is pinned when scroll down the page with header set', () => {
- element._headerHeight = 20;
- sinon.stub(element, '_getScrollY').callsFake(() => 25);
- window.scrollY = 100;
- element._handleBodyScroll();
- assert.isTrue(element.$.nav.classList.contains('pinned'));
- });
-});
-
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.ts b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.ts
new file mode 100644
index 0000000..5ccb94c
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.ts
@@ -0,0 +1,67 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import './gr-page-nav';
+import {GrPageNav} from './gr-page-nav';
+import {html} from '@polymer/polymer/lib/utils/html-tag';
+import {queryAndAssert} from '../../../test/test-utils';
+
+const basicFixture = fixtureFromTemplate(html`
+ <gr-page-nav>
+ <ul>
+ <li>item</li>
+ </ul>
+ </gr-page-nav>
+`);
+
+suite('gr-page-nav tests', () => {
+ let element: GrPageNav;
+
+ setup(async () => {
+ element = basicFixture.instantiate() as GrPageNav;
+ await element.updateComplete;
+ });
+
+ test('header is not pinned just below top', () => {
+ sinon.stub(element, 'getOffsetTop').callsFake(() => 10);
+ sinon.stub(element, 'getScrollY').callsFake(() => 5);
+ element.handleBodyScroll();
+ assert.isFalse(queryAndAssert(element, 'nav').classList.contains('pinned'));
+ });
+
+ test('header is pinned when scroll down the page', () => {
+ sinon.stub(element, 'getOffsetTop').callsFake(() => 10);
+ sinon.stub(element, 'getScrollY').callsFake(() => 25);
+ element.handleBodyScroll();
+ assert.isTrue(queryAndAssert(element, 'nav').classList.contains('pinned'));
+ });
+
+ test('header is not pinned just below top with header set', () => {
+ element.headerHeight = 20;
+ sinon.stub(element, 'getScrollY').callsFake(() => 15);
+ element.handleBodyScroll();
+ assert.isFalse(queryAndAssert(element, 'nav').classList.contains('pinned'));
+ });
+
+ test('header is pinned when scroll down the page with header set', () => {
+ element.headerHeight = 20;
+ sinon.stub(element, 'getScrollY').callsFake(() => 25);
+ element.handleBodyScroll();
+ assert.isTrue(queryAndAssert(element, 'nav').classList.contains('pinned'));
+ });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.ts b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.ts
index bc60520..85dbbc6 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.ts
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.ts
@@ -30,7 +30,7 @@
BranchInfo,
} from '../../../types/common';
import {GrLabeledAutocomplete} from '../gr-labeled-autocomplete/gr-labeled-autocomplete';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
const SUGGESTIONS_LIMIT = 15;
const REF_PREFIX = 'refs/heads/';
@@ -62,7 +62,7 @@
@property({type: Object})
_repoQuery: AutocompleteQuery = () => Promise.resolve([]);
- private readonly restApiService = appContext.restApiService;
+ private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-impl.ts
similarity index 95%
rename from polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
rename to polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-impl.ts
index dd2c8d3..370bde7 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-impl.ts
@@ -32,7 +32,8 @@
import {GrReviewerUpdatesParser} from './gr-reviewer-updates-parser';
import {parseDate} from '../../../utils/date-util';
import {getBaseUrl} from '../../../utils/url-util';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
+import {Finalizable} from '../../../services/registry';
import {getParentIndex, isMergeParent} from '../../../utils/patch-set-util';
import {
ListChangesOption,
@@ -48,7 +49,6 @@
AccountId,
AccountInfo,
ActionNameToActionInfoMap,
- AssigneeInput,
Base64File,
Base64FileContent,
Base64ImageFile,
@@ -149,7 +149,6 @@
createDefaultDiffPrefs,
createDefaultEditPrefs,
createDefaultPreferences,
- DiffViewMode,
HttpMethod,
ReviewerState,
} from '../../../constants/constants';
@@ -157,10 +156,9 @@
import {ParsedChangeInfo} from '../../../types/types';
import {ErrorCallback} from '../../../api/rest';
import {FlagsService, KnownExperimentId} from '../../../services/flags/flags';
+import {addDraftProp, DraftInfo} from '../../../utils/comment-util';
const MAX_PROJECT_RESULTS = 25;
-// This value is somewhat arbitrary and not based on research or calculations.
-const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 850;
const Requests = {
SEND_DIFF_DRAFT: 'sendDiffDraft',
@@ -180,6 +178,12 @@
let grEtagDecorator = new GrEtagDecorator(); // Shared across instances.
let projectLookup: {[changeNum: string]: RepoName} = {}; // Shared across instances.
+function suppress404s(res?: Response | null) {
+ if (!res || res.status === 404) return;
+ // This is the default error handling behavior of the rest-api-helper.
+ fireServerError(res);
+}
+
interface FetchChangeJSON {
reportEndpointAsIs?: boolean;
endpoint: string;
@@ -267,19 +271,19 @@
pendingRequest = {};
grEtagDecorator = new GrEtagDecorator();
projectLookup = {};
- appContext.authService.clearCache();
+ getAppContext().authService.clearCache();
}
declare global {
interface HTMLElementTagNameMap {
- 'gr-rest-api-interface': GrRestApiInterface;
+ 'gr-rest-api-service-impl': GrRestApiServiceImpl;
}
}
-@customElement('gr-rest-api-interface')
-export class GrRestApiInterface
+@customElement('gr-rest-api-service-impl')
+export class GrRestApiServiceImpl
extends PolymerElement
- implements RestApiService
+ implements RestApiService, Finalizable
{
readonly _cache = siteBasedCache; // Shared across instances.
@@ -303,8 +307,8 @@
super();
// TODO: Make the authService constructor parameter required when we have
// changed all usages of this class to not instantiate via createElement().
- this.authService = authService ?? appContext.authService;
- this.flagService = flagService ?? appContext.flagsService;
+ this.authService = authService ?? getAppContext().authService;
+ this.flagService = flagService ?? getAppContext().flagsService;
this._restApiHelper = new GrRestApiHelper(
this._cache,
this.authService,
@@ -312,6 +316,8 @@
);
}
+ finalize() {}
+
_fetchSharedCacheURL(req: FetchJSONRequest): Promise<ParsedJSON | undefined> {
// Cache is shared across instances
return this._restApiHelper.fetchCacheURL(req);
@@ -501,7 +507,8 @@
});
}
- getIsGroupOwner(groupName: GroupName): Promise<boolean> {
+ getIsGroupOwner(groupName?: GroupName): Promise<boolean> {
+ if (!groupName) return Promise.resolve(false);
const encodeName = encodeURIComponent(groupName);
const req = {
url: `/groups/?owned&g=${encodeName}`,
@@ -978,13 +985,6 @@
return res;
}
const prefInfo = res as unknown as PreferencesInfo;
- if (this._isNarrowScreen()) {
- // Note that this can be problematic, because the diff will stay
- // unified even after increasing the window width.
- prefInfo.default_diff_view = DiffViewMode.UNIFIED;
- } else {
- prefInfo.default_diff_view = prefInfo.diff_view;
- }
return prefInfo;
});
}
@@ -1020,10 +1020,6 @@
});
}
- _isNarrowScreen() {
- return window.innerWidth < MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX;
- }
-
getChanges(
changesPerPage?: number,
query?: string,
@@ -1124,10 +1120,11 @@
}
getChangeDetail(
- changeNum: NumericChangeId,
+ changeNum?: NumericChangeId,
errFn?: ErrorCallback,
cancelCondition?: CancelConditionCallback
- ): Promise<ParsedChangeInfo | null | undefined> {
+ ): Promise<ParsedChangeInfo | undefined> {
+ if (!changeNum) return Promise.resolve(undefined);
return this.getConfig(false).then(config => {
const optionsHex = this._getChangeOptionsHex(config);
return this._getChangeDetail(
@@ -1146,7 +1143,8 @@
_getChangesOptionsHex() {
if (
window.DEFAULT_DETAIL_HEXES &&
- window.DEFAULT_DETAIL_HEXES.dashboardPage
+ window.DEFAULT_DETAIL_HEXES.dashboardPage &&
+ !this.flagService?.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI)
) {
return window.DEFAULT_DETAIL_HEXES.dashboardPage;
}
@@ -1154,6 +1152,9 @@
ListChangesOption.LABELS,
ListChangesOption.DETAILED_ACCOUNTS,
];
+ if (this.flagService?.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI)) {
+ options.push(ListChangesOption.SUBMIT_REQUIREMENTS);
+ }
return listChangesOptionsToHex(...options);
}
@@ -1162,8 +1163,7 @@
if (
window.DEFAULT_DETAIL_HEXES &&
window.DEFAULT_DETAIL_HEXES.changePage &&
- (!config || !(config.receive && config.receive.enable_signed_push)) &&
- !this.flagService?.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI)
+ (!config || !(config.receive && config.receive.enable_signed_push))
) {
return window.DEFAULT_DETAIL_HEXES.changePage;
}
@@ -1180,30 +1180,14 @@
ListChangesOption.SUBMITTABLE,
ListChangesOption.WEB_LINKS,
ListChangesOption.SKIP_DIFFSTAT,
+ ListChangesOption.SUBMIT_REQUIREMENTS,
];
if (config?.receive?.enable_signed_push) {
options.push(ListChangesOption.PUSH_CERTIFICATES);
}
- if (this.flagService?.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI)) {
- options.push(ListChangesOption.SUBMIT_REQUIREMENTS);
- }
return listChangesOptionsToHex(...options);
}
- getDiffChangeDetail(changeNum: NumericChangeId) {
- let optionsHex = '';
- if (window.DEFAULT_DETAIL_HEXES?.diffPage) {
- optionsHex = window.DEFAULT_DETAIL_HEXES.diffPage;
- } else {
- optionsHex = listChangesOptionsToHex(
- ListChangesOption.ALL_COMMITS,
- ListChangesOption.ALL_REVISIONS,
- ListChangesOption.SKIP_DIFFSTAT
- );
- }
- return this._getChangeDetail(changeNum, optionsHex);
- }
-
/**
* @param optionsHex list changes options in hex
*/
@@ -1212,7 +1196,7 @@
optionsHex: string,
errFn?: ErrorCallback,
cancelCondition?: CancelConditionCallback
- ): Promise<ChangeInfo | undefined | null> {
+ ): Promise<ChangeInfo | undefined> {
return this.getChangeActionURL(changeNum, undefined, '/detail').then(
url => {
const params: FetchParams = {O: optionsHex};
@@ -1243,12 +1227,12 @@
}
if (!response) {
- return Promise.resolve(null);
+ return Promise.resolve(undefined);
}
return readResponsePayload(response).then(payload => {
if (!payload) {
- return null;
+ return undefined;
}
this._etags.collect(urlWithParams, response, payload.raw);
// TODO(TS): Why it is always change info?
@@ -1267,6 +1251,7 @@
endpoint: '/commit?links',
revision: patchNum,
reportEndpointAsIs: true,
+ errFn: suppress404s,
}) as Promise<CommitInfo | undefined>;
}
@@ -1713,11 +1698,12 @@
}
getChangesSubmittedTogether(
- changeNum: NumericChangeId
+ changeNum: NumericChangeId,
+ options: string[] = ['NON_VISIBLE_CHANGES']
): Promise<SubmittedTogetherInfo | undefined> {
return this._getChangeURLAndFetch({
changeNum,
- endpoint: '/submitted_together?o=NON_VISIBLE_CHANGES',
+ endpoint: `/submitted_together?o=${options.join('&o=')}`,
reportEndpointAsIs: true,
}) as Promise<SubmittedTogetherInfo | undefined>;
}
@@ -1768,22 +1754,27 @@
getChangesWithSameTopic(
topic: string,
- changeNum: NumericChangeId
+ options?: {
+ openChangesOnly?: boolean;
+ changeToExclude?: NumericChangeId;
+ }
): Promise<ChangeInfo[] | undefined> {
- const options = listChangesOptionsToHex(
+ const requestOptions = listChangesOptionsToHex(
ListChangesOption.LABELS,
ListChangesOption.CURRENT_REVISION,
ListChangesOption.CURRENT_COMMIT,
ListChangesOption.DETAILED_LABELS
);
- const query = [
- 'status:open',
- `-change:${changeNum}`,
- `topic:"${topic}"`,
- ].join(' ');
+ const queryTerms = [`topic:"${topic}"`];
+ if (options?.openChangesOnly) {
+ queryTerms.push('status:open');
+ }
+ if (options?.changeToExclude !== undefined) {
+ queryTerms.push(`-change:${options.changeToExclude}`);
+ }
const params = {
- O: options,
- q: query,
+ O: requestOptions,
+ q: queryTerms.join(' '),
};
return this._restApiHelper.fetchJSON({
url: '/changes/',
@@ -1861,14 +1852,12 @@
);
}
- getChangeEdit(
- changeNum: NumericChangeId,
- downloadCommands?: boolean
- ): Promise<false | EditInfo | undefined> {
- const params = downloadCommands ? {'download-commands': true} : undefined;
+ getChangeEdit(changeNum?: NumericChangeId): Promise<EditInfo | undefined> {
+ if (!changeNum) return Promise.resolve(undefined);
+ const params = {'download-commands': true};
return this.getLoggedIn().then(loggedIn => {
if (!loggedIn) {
- return Promise.resolve(false);
+ return Promise.resolve(undefined);
}
return this._getChangeURLAndFetch(
{
@@ -1878,7 +1867,7 @@
reportEndpointAsIs: true,
},
true
- ) as Promise<EditInfo | false | undefined>;
+ ) as Promise<EditInfo | undefined>;
});
}
@@ -1917,12 +1906,6 @@
): Promise<Response | Base64FileContent | undefined> {
// 404s indicate the file does not exist yet in the revision, so suppress
// them.
- const suppress404s: ErrorCallback = res => {
- if (res && res?.status !== 404) {
- fireServerError(res);
- }
- return res;
- };
const promise =
patchNum === EditPatchSetNum
? this._getFileInChangeEdit(changeNum, path)
@@ -2294,45 +2277,16 @@
* is no logged in user, the request is not made and the promise yields an
* empty object.
*/
- getDiffDrafts(
+ async getDiffDrafts(
changeNum: NumericChangeId
- ): Promise<PathToCommentsInfoMap | undefined>;
-
- getDiffDrafts(
- changeNum: NumericChangeId,
- basePatchNum: BasePatchSetNum,
- patchNum: PatchSetNum,
- path: string
- ): Promise<GetDiffCommentsOutput>;
-
- getDiffDrafts(
- changeNum: NumericChangeId,
- basePatchNum?: BasePatchSetNum,
- patchNum?: PatchSetNum,
- path?: string
- ) {
- return this.getLoggedIn().then(loggedIn => {
- if (!loggedIn) {
- return {};
- }
- if (!basePatchNum && !patchNum && !path) {
- return this._getDiffComments(changeNum, '/drafts', {
- 'enable-context': true,
- 'context-padding': 3,
- });
- }
- return this._getDiffComments(
- changeNum,
- '/drafts',
- {
- 'enable-context': true,
- 'context-padding': 3,
- },
- basePatchNum,
- patchNum,
- path
- );
+ ): Promise<{[path: string]: DraftInfo[]} | undefined> {
+ const loggedIn = await this.getLoggedIn();
+ if (!loggedIn) return {};
+ const comments = await this._getDiffComments(changeNum, '/drafts', {
+ 'enable-context': true,
+ 'context-padding': 3,
});
+ return addDraftProp(comments);
}
_setRange(comments: CommentInfo[], comment: CommentInfo) {
@@ -2951,29 +2905,6 @@
}) as Promise<TopMenuEntryInfo[] | undefined>;
}
- setAssignee(
- changeNum: NumericChangeId,
- assignee: AccountId
- ): Promise<Response> {
- const body: AssigneeInput = {assignee};
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.PUT,
- endpoint: '/assignee',
- body,
- reportUrlAsIs: true,
- });
- }
-
- deleteAssignee(changeNum: NumericChangeId): Promise<Response> {
- return this._getChangeURLAndSend({
- changeNum,
- method: HttpMethod.DELETE,
- endpoint: '/assignee',
- reportUrlAsIs: true,
- });
- }
-
probePath(path: string) {
return fetch(new Request(path, {method: HttpMethod.HEAD})).then(
response => response.ok
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-impl_test.js
similarity index 97%
rename from polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js
rename to polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-impl_test.js
index a60a1ef..b3f751a 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-impl_test.js
@@ -19,7 +19,7 @@
import {addListenerForTest, mockPromise, stubAuth} from '../../../test/test-utils.js';
import {GrReviewerUpdatesParser} from './gr-reviewer-updates-parser.js';
import {ListChangesOption} from '../../../utils/change-util.js';
-import {appContext} from '../../../services/app-context.js';
+import {getAppContext} from '../../../services/app-context.js';
import {createChange} from '../../../test/test-data-generators.js';
import {CURRENT} from '../../../utils/patch-set-util.js';
import {
@@ -27,9 +27,9 @@
readResponsePayload,
} from './gr-rest-apis/gr-rest-api-helper.js';
import {JSON_PREFIX} from './gr-rest-apis/gr-rest-api-helper.js';
-import {GrRestApiInterface} from './gr-rest-api-interface.js';
+import {GrRestApiServiceImpl} from './gr-rest-api-impl.js';
-suite('gr-rest-api-interface tests', () => {
+suite('gr-rest-api-service-impl tests', () => {
let element;
let ctr = 0;
@@ -49,9 +49,12 @@
},
}));
// fake auth
- sinon.stub(appContext.authService, 'authCheck')
+ sinon.stub(getAppContext().authService, 'authCheck')
.returns(Promise.resolve(true));
- element = new GrRestApiInterface();
+ element = new GrRestApiServiceImpl(
+ getAppContext().authService,
+ getAppContext().flagsService
+ );
element._projectLookup = {};
});
@@ -333,26 +336,23 @@
stub.lastCall.args[0].errFn({});
});
- const preferenceSetup = function(testJSON, loggedIn, smallScreen) {
+ const preferenceSetup = function(testJSON, loggedIn) {
sinon.stub(element, 'getLoggedIn')
.callsFake(() => Promise.resolve(loggedIn));
- sinon.stub(element, '_isNarrowScreen').callsFake(() => smallScreen);
sinon.stub(
element._restApiHelper,
'fetchCacheURL')
.callsFake(() => Promise.resolve(testJSON));
};
- test('getPreferences returns correctly on small screens logged in',
+ test('getPreferences returns correctly logged in',
() => {
const testJSON = {diff_view: 'SIDE_BY_SIDE'};
const loggedIn = true;
- const smallScreen = true;
- preferenceSetup(testJSON, loggedIn, smallScreen);
+ preferenceSetup(testJSON, loggedIn);
return element.getPreferences().then(obj => {
- assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
});
});
@@ -361,12 +361,10 @@
() => {
const testJSON = {diff_view: 'UNIFIED_DIFF'};
const loggedIn = true;
- const smallScreen = false;
- preferenceSetup(testJSON, loggedIn, smallScreen);
+ preferenceSetup(testJSON, loggedIn);
return element.getPreferences().then(obj => {
- assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
assert.equal(obj.diff_view, 'UNIFIED_DIFF');
});
});
@@ -375,12 +373,10 @@
() => {
const testJSON = {diff_view: 'UNIFIED_DIFF'};
const loggedIn = false;
- const smallScreen = false;
- preferenceSetup(testJSON, loggedIn, smallScreen);
+ preferenceSetup(testJSON, loggedIn);
return element.getPreferences().then(obj => {
- assert.equal(obj.default_diff_view, 'SIDE_BY_SIDE');
assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
});
});
@@ -835,7 +831,7 @@
test('gerrit auth is used', () => {
stubAuth('fetch').returns(Promise.resolve());
element._restApiHelper.fetchJSON({url: 'foo'});
- assert(appContext.authService.fetch.called);
+ assert(getAppContext().authService.fetch.called);
});
test('getSuggestedAccounts does not return _fetchJSON', () => {
@@ -1251,7 +1247,8 @@
const res = {status: 404};
const spy = sinon.spy();
addListenerForTest(document, 'server-error', spy);
- sinon.stub(appContext.authService, 'fetch').returns(Promise.resolve(res));
+ sinon.stub(getAppContext().authService, 'fetch')
+ .returns(Promise.resolve(res));
sinon.stub(element, '_changeBaseURL').returns(Promise.resolve(''));
return element.getFileContent('1', 'tst/path', '1')
.then(() => {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.js
index 70bd369..e520287 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.js
@@ -17,7 +17,7 @@
import '../../../../test/common-test-setup-karma.js';
import {SiteBasedCache, FetchPromisesCache, GrRestApiHelper} from './gr-rest-api-helper.js';
-import {appContext} from '../../../../services/app-context.js';
+import {getAppContext} from '../../../../services/app-context.js';
import {stubAuth} from '../../../../test/test-utils.js';
suite('gr-rest-api-helper tests', () => {
@@ -47,7 +47,7 @@
},
}));
- helper = new GrRestApiHelper(cache, appContext.authService,
+ helper = new GrRestApiHelper(cache, getAppContext().authService,
fetchPromisesCache, mockRestApiInterface);
});
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.ts
index a603ec6..da5d331 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.ts
@@ -276,8 +276,8 @@
}
static parse(
- change: ChangeViewChangeInfo | undefined | null
- ): ParsedChangeInfo | undefined | null {
+ change: ChangeViewChangeInfo | undefined
+ ): ParsedChangeInfo | undefined {
// TODO(TS): The !change condition should be removed when all files are converted to TS
if (!change || !isChangeInfoParserInput(change)) {
return change;
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.ts b/polygerrit-ui/app/elements/shared/gr-select/gr-select.ts
index 571272d..47295ab 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.ts
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.ts
@@ -34,7 +34,7 @@
}
@property({type: String, notify: true})
- bindValue?: string | number;
+ bindValue?: string | number | boolean;
get nativeSelect() {
// gr-select is not a shadow component
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 9e6b42a..b602a87 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -22,9 +22,8 @@
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-textarea_html';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {customElement, property} from '@polymer/decorators';
-import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
import {
GrAutocompleteDropdown,
@@ -32,6 +31,7 @@
ItemSelectedEvent,
} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
import {addShortcut, Key} from '../../../utils/dom-util';
+import {BindValueChangeEvent} from '../../../types/events';
const MAX_ITEMS_DROPDOWN = 10;
@@ -63,10 +63,6 @@
match: string;
}
-interface ValueChangeEvent {
- value: string;
-}
-
export interface GrTextarea {
$: {
textarea: IronAutogrowTextareaElement;
@@ -79,7 +75,6 @@
declare global {
interface HTMLElementEventMap {
'item-selected': CustomEvent<ItemSelectedEvent>;
- 'bind-value-changed': CustomEvent<ValueChangeEvent>;
}
}
@@ -141,18 +136,14 @@
readonly _verticalOffset = 20;
// Offset makes dropdown appear below text.
- reporting: ReportingService;
+ // Accessed in tests.
+ readonly reporting = getAppContext().reportingService;
disableEnterKeyForSelectingEmoji = false;
/** Called in disconnectedCallback. */
private cleanups: (() => void)[] = [];
- constructor() {
- super();
- this.reporting = appContext.reportingService;
- }
-
override disconnectedCallback() {
super.disconnectedCallback();
for (const cleanup of this.cleanups) cleanup();
@@ -162,19 +153,29 @@
override connectedCallback() {
super.connectedCallback();
this.cleanups.push(
- addShortcut(this, {key: Key.UP}, e => this._handleUpKey(e))
+ addShortcut(this, {key: Key.UP}, e => this._handleUpKey(e), {
+ doNotPrevent: true,
+ })
);
this.cleanups.push(
- addShortcut(this, {key: Key.DOWN}, e => this._handleDownKey(e))
+ addShortcut(this, {key: Key.DOWN}, e => this._handleDownKey(e), {
+ doNotPrevent: true,
+ })
);
this.cleanups.push(
- addShortcut(this, {key: Key.TAB}, e => this._handleTabKey(e))
+ addShortcut(this, {key: Key.TAB}, e => this._handleTabKey(e), {
+ doNotPrevent: true,
+ })
);
this.cleanups.push(
- addShortcut(this, {key: Key.ENTER}, e => this._handleEnterByKey(e))
+ addShortcut(this, {key: Key.ENTER}, e => this._handleEnterByKey(e), {
+ doNotPrevent: true,
+ })
);
this.cleanups.push(
- addShortcut(this, {key: Key.ESC}, e => this._handleEscKey(e))
+ addShortcut(this, {key: Key.ESC}, e => this._handleEscKey(e), {
+ doNotPrevent: true,
+ })
);
}
@@ -316,7 +317,7 @@
* _handleKeydown used for key handling in the this.$.textarea AND all child
* autocomplete options.
*/
- _onValueChanged(e: CustomEvent<ValueChangeEvent>) {
+ _onValueChanged(e: BindValueChangeEvent) {
// Relay the event.
this.dispatchEvent(
new CustomEvent('bind-value-changed', {
@@ -423,6 +424,9 @@
}
_handleTextChanged(text: string) {
+ // This is a bit redundant, because the `text` property has `notify:true`,
+ // so whenever the `text` changes the component fires two identical events
+ // `text-changed` and `value-changed`.
this.dispatchEvent(
new CustomEvent('value-changed', {detail: {value: text}})
);
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 0585aec8..e387a3d 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
@@ -123,7 +123,7 @@
this.addEventListener('mouseenter', this.showHandler);
}
- _handleShowTooltip() {
+ async _handleShowTooltip() {
if (this.isTouchDevice) {
return;
}
@@ -145,15 +145,16 @@
tooltip.text = this.originalTitle;
tooltip.maxWidth = this.getAttribute('max-width') || '';
tooltip.positionBelow = this.hasAttribute('position-below');
+ this.tooltip = tooltip;
// 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);
+ await tooltip.updateComplete;
this._positionTooltip(tooltip);
tooltip.style.visibility = 'initial';
- this.tooltip = tooltip;
window.addEventListener('scroll', this.windowScrollHandler);
this.addEventListener('mouseleave', this.hideHandler);
this.addEventListener('click', this.hideHandler);
@@ -198,7 +199,7 @@
}
// private but used in tests.
- async _positionTooltip(tooltip: GrTooltip | null) {
+ _positionTooltip(tooltip: GrTooltip | null) {
if (tooltip === null) return;
const rect = this.getBoundingClientRect();
const boxRect = tooltip.getBoundingClientRect();
@@ -210,13 +211,9 @@
const left = rect.left - parentRect.left + (rect.width - boxRect.width) / 2;
const right = parentRect.width - left - boxRect.width;
if (left < 0) {
- tooltip.updateStyles({
- '--gr-tooltip-arrow-center-offset': `${left}px`,
- });
+ tooltip.arrowCenterOffset = `${left}px`;
} else if (right < 0) {
- tooltip.updateStyles({
- '--gr-tooltip-arrow-center-offset': `${-0.5 * right}px`,
- });
+ tooltip.arrowCenterOffset = `${-0.5 * right}px`;
}
tooltip.style.left = `${Math.max(0, left)}px`;
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js
index 8d3bbb0..d47c20b 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js
@@ -25,11 +25,15 @@
function makeTooltip(tooltipRect, parentRect) {
return {
- getBoundingClientRect() { return tooltipRect; },
- updateStyles: sinon.stub(),
+ arrowCenterOffset: '0',
+ getBoundingClientRect() {
+ return tooltipRect;
+ },
style: {left: 0, top: 0},
parentElement: {
- getBoundingClientRect() { return parentRect; },
+ getBoundingClientRect() {
+ return parentRect;
+ },
},
};
}
@@ -66,12 +70,12 @@
{top: 0, left: 0, width: 1000});
element._positionTooltip(tooltip);
- assert.isFalse(tooltip.updateStyles.called);
+ assert.equal(tooltip.arrowCenterOffset, '0');
assert.equal(tooltip.style.left, '175px');
assert.equal(tooltip.style.top, '100px');
});
- test('left side position', () => {
+ test('left side position', async () => {
sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
return {top: 100, left: 10, width: 50};
});
@@ -80,10 +84,8 @@
{top: 0, left: 0, width: 1000});
element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isBelow(parseFloat(offset.replace(/px$/, '')), 0);
+ await element.updateComplete;
+ assert.isBelow(parseFloat(tooltip.arrowCenterOffset.replace(/px$/, '')), 0);
assert.equal(tooltip.style.left, '0px');
assert.equal(tooltip.style.top, '100px');
});
@@ -97,10 +99,7 @@
{top: 0, left: 0, width: 1000});
element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
+ assert.isAbove(parseFloat(tooltip.arrowCenterOffset.replace(/px$/, '')), 0);
assert.equal(tooltip.style.left, '915px');
assert.equal(tooltip.style.top, '100px');
});
@@ -115,19 +114,16 @@
element.positionBelow = true;
element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
+ assert.isAbove(parseFloat(tooltip.arrowCenterOffset.replace(/px$/, '')), 0);
assert.equal(tooltip.style.left, '915px');
assert.equal(tooltip.style.top, '157.2px');
});
test('hides tooltip when detached', async () => {
- sinon.stub(element, '_handleHideTooltip');
+ const handleHideTooltipStub = sinon.stub(element, '_handleHideTooltip');
element.remove();
await element.updateComplete;
- assert.isTrue(element._handleHideTooltip.called);
+ assert.isTrue(handleHideTooltipStub.called);
});
test('sets up listeners when has-tooltip is changed', async () => {
@@ -152,7 +148,7 @@
await element.updateComplete;
// fire mouse-enter
- element._handleShowTooltip();
+ await element._handleShowTooltip();
await element.updateComplete;
assert.isNotOk(element.tooltip);
@@ -165,7 +161,7 @@
element.isTouchDevice = false;
// fire mouse-enter
- element._handleShowTooltip();
+ await element._handleShowTooltip();
await element.updateComplete;
assert.isOk(element.tooltip);
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts
index cab05b4..0e41891 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts
@@ -14,14 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-tooltip_html';
-import {customElement, property, observe} from '@polymer/decorators';
-export interface GrTooltip {
- $: {};
-}
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
+import {styleMap} from 'lit/directives/style-map';
declare global {
interface HTMLElementTagNameMap {
@@ -30,22 +27,78 @@
}
@customElement('gr-tooltip')
-export class GrTooltip extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrTooltip extends LitElement {
@property({type: String})
text = '';
@property({type: String})
maxWidth = '';
- @property({type: Boolean, reflectToAttribute: true})
+ @property({type: String})
+ arrowCenterOffset = '0';
+
+ @property({type: Boolean, reflect: true, attribute: 'position-below'})
positionBelow = false;
- @observe('maxWidth')
- _updateWidth(maxWidth: string) {
- this.updateStyles({'--tooltip-max-width': maxWidth});
+ static override get styles() {
+ return [
+ sharedStyles,
+ css`
+ :host {
+ --gr-tooltip-arrow-size: 0.5em;
+
+ background-color: var(--tooltip-background-color);
+ box-shadow: var(--elevation-level-2);
+ color: var(--tooltip-text-color);
+ font-size: var(--font-size-small);
+ position: absolute;
+ z-index: 1000;
+ }
+ :host .tooltip {
+ padding: var(--spacing-m) var(--spacing-l);
+ }
+ :host .arrowPositionBelow,
+ :host([position-below]) .arrowPositionAbove {
+ display: none;
+ }
+ :host([position-below]) .arrowPositionBelow {
+ display: initial;
+ }
+ .arrow {
+ border-left: var(--gr-tooltip-arrow-size) solid transparent;
+ border-right: var(--gr-tooltip-arrow-size) solid transparent;
+ height: 0;
+ position: absolute;
+ left: calc(50% - var(--gr-tooltip-arrow-size));
+ width: 0;
+ }
+ .arrowPositionAbove {
+ border-top: var(--gr-tooltip-arrow-size) solid
+ var(--tooltip-background-color);
+ bottom: calc(-1 * var(--gr-tooltip-arrow-size));
+ }
+ .arrowPositionBelow {
+ border-bottom: var(--gr-tooltip-arrow-size) solid
+ var(--tooltip-background-color);
+ top: calc(-1 * var(--gr-tooltip-arrow-size));
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ this.style.maxWidth = this.maxWidth;
+
+ return html` <div class="tooltip">
+ <i
+ class="arrowPositionBelow arrow"
+ style="${styleMap({marginLeft: this.arrowCenterOffset})}"
+ ></i>
+ ${this.text}
+ <i
+ class="arrowPositionAbove arrow"
+ style="${styleMap({marginLeft: this.arrowCenterOffset})}"
+ ></i>
+ </div>`;
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.ts b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.ts
deleted file mode 100644
index d59a6c3..0000000
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- --gr-tooltip-arrow-size: 0.5em;
- --gr-tooltip-arrow-center-offset: 0;
-
- background-color: var(--tooltip-background-color);
- box-shadow: var(--elevation-level-2);
- color: var(--tooltip-text-color);
- font-size: var(--font-size-small);
- position: absolute;
- z-index: 1000;
- max-width: var(--tooltip-max-width);
- }
- :host .tooltip {
- padding: var(--spacing-m) var(--spacing-l);
- }
- :host .arrowPositionBelow,
- :host([position-below]) .arrowPositionAbove {
- display: none;
- }
- :host([position-below]) .arrowPositionBelow {
- display: initial;
- }
- .arrow {
- border-left: var(--gr-tooltip-arrow-size) solid transparent;
- border-right: var(--gr-tooltip-arrow-size) solid transparent;
- height: 0;
- position: absolute;
- left: calc(50% - var(--gr-tooltip-arrow-size));
- margin-left: var(--gr-tooltip-arrow-center-offset);
- width: 0;
- }
- .arrowPositionAbove {
- border-top: var(--gr-tooltip-arrow-size) solid
- var(--tooltip-background-color);
- bottom: calc(-1 * var(--gr-tooltip-arrow-size));
- }
- .arrowPositionBelow {
- border-bottom: var(--gr-tooltip-arrow-size) solid
- var(--tooltip-background-color);
- top: calc(-1 * var(--gr-tooltip-arrow-size));
- }
- </style>
- <div class="tooltip">
- <i class="arrowPositionBelow arrow"></i>
- [[text]]
- <i class="arrowPositionAbove arrow"></i>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts
index 65ceab1..b693a9e 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts
@@ -17,50 +17,45 @@
import '../../../test/common-test-setup-karma';
import './gr-tooltip';
-import {html} from '@polymer/polymer/lib/utils/html-tag';
import {GrTooltip} from './gr-tooltip';
+import {queryAndAssert} from '../../../test/test-utils';
-const basicFixture = fixtureFromTemplate(html` <gr-tooltip> </gr-tooltip> `);
+const basicFixture = fixtureFromElement('gr-tooltip');
suite('gr-tooltip tests', () => {
let element: GrTooltip;
+
setup(async () => {
element = basicFixture.instantiate() as GrTooltip;
- await flush();
+ await element.updateComplete;
});
- test('max-width is respected if set', () => {
+ test('max-width is respected if set', async () => {
element.text =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit' +
', sed do eiusmod tempor incididunt ut labore et dolore magna aliqua';
element.maxWidth = '50px';
+ await element.updateComplete;
assert.equal(getComputedStyle(element).width, '50px');
});
- test('the correct arrow is displayed', () => {
+ test('the correct arrow is displayed', async () => {
assert.equal(
- getComputedStyle(
- element.shadowRoot!.querySelector('.arrowPositionBelow')!
- ).display,
+ getComputedStyle(queryAndAssert(element, '.arrowPositionBelow')!).display,
'none'
);
assert.notEqual(
- getComputedStyle(
- element.shadowRoot!.querySelector('.arrowPositionAbove')!
- ).display,
+ getComputedStyle(queryAndAssert(element, '.arrowPositionAbove')!).display,
'none'
);
element.positionBelow = true;
+ await element.updateComplete;
assert.notEqual(
- getComputedStyle(
- element.shadowRoot!.querySelector('.arrowPositionBelow')!
- ).display,
+ getComputedStyle(queryAndAssert(element, '.arrowPositionBelow')!).display,
'none'
);
assert.equal(
- getComputedStyle(
- element.shadowRoot!.querySelector('.arrowPositionAbove')!
- ).display,
+ getComputedStyle(queryAndAssert(element, '.arrowPositionAbove')!).display,
'none'
);
});
diff --git a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
index 9013088..ba7dc36 100644
--- a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip.ts
@@ -22,7 +22,7 @@
isQuickLabelInfo,
LabelInfo,
} from '../../../api/rest-api';
-import {appContext} from '../../../services/app-context';
+import {getAppContext} from '../../../services/app-context';
import {KnownExperimentId} from '../../../services/flags/flags';
import {
classForLabelStatus,
@@ -48,7 +48,7 @@
@property({type: Boolean})
more = false;
- private readonly flagsService = appContext.flagsService;
+ private readonly flagsService = getAppContext().flagsService;
static override get styles() {
return [
@@ -57,7 +57,7 @@
background-color: var(--vote-color-approved);
padding: 2px;
}
- .vote-chip.max.more {
+ .more > .vote-chip.max {
padding: 1px;
border: 1px solid var(--vote-outline-recommended);
}
@@ -65,7 +65,7 @@
background-color: var(--vote-color-rejected);
padding: 2px;
}
- .vote-chip.min.more {
+ .more > .vote-chip.min {
padding: 1px;
border: 1px solid var(--vote-outline-disliked);
}
@@ -93,12 +93,13 @@
padding: 1px;
border-radius: var(--border-radius);
line-height: var(--gr-vote-chip-width, 16px);
+ color: var(--vote-text-color);
}
- .vote-chip {
+ .more > .vote-chip {
position: relative;
z-index: 2;
}
- .chip-angle {
+ .more > .chip-angle {
position: absolute;
top: 2px;
left: 2px;
@@ -118,10 +119,8 @@
const renderValue = this.renderValue();
if (!renderValue) return;
- return html`<span class="container">
- <div class="vote-chip ${this.computeClass()} ${this.more ? 'more' : ''}">
- ${renderValue}
- </div>
+ return html`<span class="container ${this.more ? 'more' : ''}">
+ <div class="vote-chip ${this.computeClass()}">${renderValue}</div>
${this.more
? html`<div class="chip-angle ${this.computeClass()}">
${renderValue}
@@ -139,9 +138,11 @@
}
} else if (isQuickLabelInfo(this.label)) {
if (this.label.approved) {
- return '👍️';
+ return '👍';
} else if (this.label.rejected) {
- return '👎️';
+ return '👎';
+ } else if (this.label.disliked || this.label.recommended) {
+ return valueString(this.label.value);
}
}
return '';
diff --git a/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip_test.ts b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip_test.ts
new file mode 100644
index 0000000..d5c5df8
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-vote-chip/gr-vote-chip_test.ts
@@ -0,0 +1,108 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import {fixture} from '@open-wc/testing-helpers';
+import {html} from 'lit';
+import {getAppContext} from '../../../services/app-context';
+import './gr-vote-chip';
+import {GrVoteChip} from './gr-vote-chip';
+import {
+ createAccountWithIdNameAndEmail,
+ createApproval,
+ createDetailedLabelInfo,
+ createQuickLabelInfo,
+} from '../../../test/test-data-generators';
+import {ApprovalInfo} from '../../../api/rest-api';
+
+suite('gr-vote-chip tests', () => {
+ setup(() => {
+ sinon.stub(getAppContext().flagsService, 'isEnabled').returns(true);
+ });
+
+ suite('with QuickLabelInfo', () => {
+ let element: GrVoteChip;
+
+ setup(async () => {
+ const labelInfo = {
+ ...createQuickLabelInfo(),
+ approved: createAccountWithIdNameAndEmail(),
+ };
+ element = await fixture<GrVoteChip>(
+ html`<gr-vote-chip .label=${labelInfo}></gr-vote-chip>`
+ );
+ });
+
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(`<span class="container">
+ <div class="max vote-chip">👍</div>
+ </span>`);
+ });
+ });
+
+ suite('with DetailedLabelInfo', () => {
+ let element: GrVoteChip;
+ const labelInfo = createDetailedLabelInfo();
+ const vote: ApprovalInfo = {
+ ...createApproval(),
+ value: 2,
+ };
+
+ setup(async () => {
+ element = await fixture<GrVoteChip>(
+ html`<gr-vote-chip .label=${labelInfo} .vote=${vote}></gr-vote-chip>`
+ );
+ });
+
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(`<span class="container">
+ <div class="positive vote-chip">
+ +2
+ </div>
+ </span>`);
+ });
+
+ test('renders negative vote', async () => {
+ const vote: ApprovalInfo = {
+ ...createApproval,
+ value: -1,
+ };
+ element = await fixture<GrVoteChip>(
+ html`<gr-vote-chip .label=${labelInfo} .vote=${vote}></gr-vote-chip>`
+ );
+ expect(element).shadowDom.to.equal(`<span class="container">
+ <div class="min vote-chip">
+ -1
+ </div>
+ </span>`);
+ });
+
+ test('renders for more than 1 vote', async () => {
+ element = await fixture<GrVoteChip>(
+ html`<gr-vote-chip
+ .label=${labelInfo}
+ .vote=${vote}
+ more
+ ></gr-vote-chip>`
+ );
+ expect(element).shadowDom.to.equal(`<span class="container more">
+ <div class="positive vote-chip">+2</div>
+ <div class="chip-angle positive">+2</div>
+ </span>`);
+ });
+ });
+});
diff --git a/polygerrit-ui/app/elements/topic/gr-topic-summary.ts b/polygerrit-ui/app/elements/topic/gr-topic-summary.ts
new file mode 100644
index 0000000..81e5686
--- /dev/null
+++ b/polygerrit-ui/app/elements/topic/gr-topic-summary.ts
@@ -0,0 +1,64 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {customElement, property, state} from 'lit/decorators';
+import {LitElement, html, PropertyValues} from 'lit-element/lit-element';
+import {getAppContext} from '../../services/app-context';
+import '../shared/gr-button/gr-button';
+
+/**
+ * A summary of a topic with buttons for performing topic-level operations.
+ */
+@customElement('gr-topic-summary')
+export class GrTopicSummary extends LitElement {
+ @property({type: String})
+ topicName?: string;
+
+ @state()
+ private changeCount?: number;
+
+ private restApiService = getAppContext().restApiService;
+
+ override willUpdate(changedProperties: PropertyValues) {
+ // TODO: receive data from the model once it is added.
+ if (changedProperties.has('topicName')) {
+ this.restApiService
+ .getChanges(undefined /* changesPerPage */, `topic:${this.topicName}`)
+ .then(response => {
+ this.changeCount = response?.length ?? 0;
+ });
+ }
+ }
+
+ override render() {
+ if (this.topicName === undefined) {
+ return;
+ }
+ return html`
+ <span>Topic: ${this.topicName}</span>
+ <span>${this.changeCount} changes</span>
+ <gr-button>Reply</gr-button>
+ <gr-button>Select Changes</gr-button>
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-topic-summary': GrTopicSummary;
+ }
+}
diff --git a/polygerrit-ui/app/elements/topic/gr-topic-summary_test.ts b/polygerrit-ui/app/elements/topic/gr-topic-summary_test.ts
new file mode 100644
index 0000000..33a9029
--- /dev/null
+++ b/polygerrit-ui/app/elements/topic/gr-topic-summary_test.ts
@@ -0,0 +1,44 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../test/common-test-setup-karma';
+import {createChange} from '../../test/test-data-generators';
+import {queryAll, stubRestApi} from '../../test/test-utils';
+import './gr-topic-summary';
+import {GrTopicSummary} from './gr-topic-summary';
+
+const basicFixture = fixtureFromElement('gr-topic-summary');
+const topicName = 'myTopic';
+
+suite('gr-topic-summary tests', () => {
+ let element: GrTopicSummary;
+
+ setup(async () => {
+ stubRestApi('getChanges')
+ .withArgs(undefined, `topic:${topicName}`)
+ .resolves([createChange(), createChange(), createChange()]);
+ element = basicFixture.instantiate();
+ element.topicName = topicName;
+ await element.updateComplete;
+ });
+
+ test('shows topic information', () => {
+ const labels = queryAll<HTMLSpanElement>(element, 'span');
+ assert.equal(labels[0].textContent, `Topic: ${topicName}`);
+ assert.equal(labels[1].textContent, '3 changes');
+ });
+});
diff --git a/polygerrit-ui/app/elements/topic/gr-topic-tree-repo.ts b/polygerrit-ui/app/elements/topic/gr-topic-tree-repo.ts
new file mode 100644
index 0000000..234f058
--- /dev/null
+++ b/polygerrit-ui/app/elements/topic/gr-topic-tree-repo.ts
@@ -0,0 +1,61 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './gr-topic-tree-row';
+import {customElement, property} from 'lit/decorators';
+import {LitElement, html, css} from 'lit-element/lit-element';
+import '../shared/gr-button/gr-button';
+import {ChangeInfo, RepoName} from '../../api/rest-api';
+
+/**
+ * A view of changes that all belong to the same repository.
+ */
+@customElement('gr-topic-tree-repo')
+export class GrTopicTreeRepo extends LitElement {
+ @property({type: String})
+ repoName?: RepoName;
+
+ @property({type: Array})
+ changes?: ChangeInfo[];
+
+ static override styles = css`
+ :host {
+ display: contents;
+ }
+ `;
+
+ override render() {
+ if (this.repoName === undefined || this.changes === undefined) {
+ return;
+ }
+ // TODO: Groups of related changes should be separated within the repository.
+ return html`
+ <h2>Repo ${this.repoName}</h2>
+ ${this.changes.map(change => this.renderTreeRow(change))}
+ `;
+ }
+
+ private renderTreeRow(change: ChangeInfo) {
+ return html`<gr-topic-tree-row .change=${change}></gr-topic-tree-row>`;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-topic-tree-repo': GrTopicTreeRepo;
+ }
+}
diff --git a/polygerrit-ui/app/elements/topic/gr-topic-tree-repo_test.ts b/polygerrit-ui/app/elements/topic/gr-topic-tree-repo_test.ts
new file mode 100644
index 0000000..2e903b5
--- /dev/null
+++ b/polygerrit-ui/app/elements/topic/gr-topic-tree-repo_test.ts
@@ -0,0 +1,42 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {RepoName} from '../../api/rest-api';
+import '../../test/common-test-setup-karma';
+import {createChange} from '../../test/test-data-generators';
+import {queryAndAssert} from '../../test/test-utils';
+import './gr-topic-tree-repo';
+import {GrTopicTreeRepo} from './gr-topic-tree-repo';
+
+const basicFixture = fixtureFromElement('gr-topic-tree-repo');
+const repoName = 'myRepo' as RepoName;
+
+suite('gr-topic-tree-repo tests', () => {
+ let element: GrTopicTreeRepo;
+
+ setup(async () => {
+ element = basicFixture.instantiate();
+ element.repoName = repoName;
+ element.changes = [createChange()];
+ await element.updateComplete;
+ });
+
+ test('shows repository name', () => {
+ const heading = queryAndAssert<HTMLHeadingElement>(element, 'h2');
+ assert.equal(heading.textContent, `Repo ${repoName}`);
+ });
+});
diff --git a/polygerrit-ui/app/elements/topic/gr-topic-tree-row.ts b/polygerrit-ui/app/elements/topic/gr-topic-tree-row.ts
new file mode 100644
index 0000000..0355bee
--- /dev/null
+++ b/polygerrit-ui/app/elements/topic/gr-topic-tree-row.ts
@@ -0,0 +1,88 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {customElement, property} from 'lit/decorators';
+import {LitElement, html, css} from 'lit-element/lit-element';
+import '../shared/gr-button/gr-button';
+import {ChangeInfo} from '../../api/rest-api';
+
+// TODO: copied from gr-change-list-item. Extract both places to a util.
+enum ChangeSize {
+ XS = 10,
+ SMALL = 50,
+ MEDIUM = 250,
+ LARGE = 1000,
+}
+
+/**
+ * A single change shown as part of the topic tree.
+ */
+@customElement('gr-topic-tree-row')
+export class GrTopicTreeRow extends LitElement {
+ @property({type: Object})
+ change?: ChangeInfo;
+
+ static override styles = css`
+ :host {
+ display: contents;
+ }
+ `;
+
+ override render() {
+ if (this.change === undefined) {
+ return;
+ }
+ const authorName =
+ this.change.revisions?.[this.change.current_revision!].commit?.author
+ .name;
+ return html`
+ <tr>
+ <td>${this.computeSize(this.change)}</td>
+ <td>${this.change.subject}</td>
+ <td>${this.change.topic}</td>
+ <td>${this.change.branch}</td>
+ <td>${authorName}</td>
+ <td>${this.change.status}</td>
+ </tr>
+ `;
+ }
+
+ // TODO: copied from gr-change-list-item. Extract both places to a util.
+ private computeSize(change: ChangeInfo) {
+ const delta = change.insertions + change.deletions;
+ if (isNaN(delta) || delta === 0) {
+ return;
+ }
+ if (delta < ChangeSize.XS) {
+ return 'XS';
+ } else if (delta < ChangeSize.SMALL) {
+ return 'S';
+ } else if (delta < ChangeSize.MEDIUM) {
+ return 'M';
+ } else if (delta < ChangeSize.LARGE) {
+ return 'L';
+ } else {
+ return 'XL';
+ }
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-topic-tree-row': GrTopicTreeRow;
+ }
+}
diff --git a/polygerrit-ui/app/elements/topic/gr-topic-tree-row_test.ts b/polygerrit-ui/app/elements/topic/gr-topic-tree-row_test.ts
new file mode 100644
index 0000000..e73cf13
--- /dev/null
+++ b/polygerrit-ui/app/elements/topic/gr-topic-tree-row_test.ts
@@ -0,0 +1,102 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {ChangeInfo, ChangeStatus, TopicName} from '../../api/rest-api';
+import '../../test/common-test-setup-karma';
+import {
+ createChangeViewChange,
+ TEST_BRANCH_ID,
+ TEST_SUBJECT,
+} from '../../test/test-data-generators';
+import {queryAll} from '../../test/test-utils';
+import './gr-topic-tree-row';
+import {GrTopicTreeRow} from './gr-topic-tree-row';
+
+const basicFixture = fixtureFromElement('gr-topic-tree-row');
+
+suite('gr-topic-tree-row tests', () => {
+ let element: GrTopicTreeRow;
+ const change: ChangeInfo = {
+ ...createChangeViewChange(),
+ insertions: 50,
+ topic: 'myTopic' as TopicName,
+ };
+
+ setup(async () => {
+ element = basicFixture.instantiate();
+ element.change = change;
+ await element.updateComplete;
+ });
+
+ test('shows columns of change information', () => {
+ const columns = queryAll<HTMLTableCellElement>(element, 'td');
+ assert.equal(columns[0].textContent, 'M');
+ assert.equal(columns[1].textContent, TEST_SUBJECT);
+ assert.equal(columns[2].textContent, 'myTopic');
+ assert.equal(columns[3].textContent, TEST_BRANCH_ID);
+ assert.equal(columns[4].textContent, 'Test name');
+ assert.equal(columns[5].textContent, ChangeStatus.NEW);
+ });
+
+ test('shows unknown size', async () => {
+ element.change = {...change, insertions: 0, deletions: 0};
+ await element.updateComplete;
+
+ const columns = queryAll<HTMLTableCellElement>(element, 'td');
+ assert.equal(columns[0].textContent, '');
+ });
+
+ test('shows XS size', async () => {
+ element.change = {...change, insertions: 3, deletions: 6};
+ await element.updateComplete;
+
+ const columns = queryAll<HTMLTableCellElement>(element, 'td');
+ assert.equal(columns[0].textContent, 'XS');
+ });
+
+ test('shows S size', async () => {
+ element.change = {...change, insertions: 9, deletions: 40};
+ await element.updateComplete;
+
+ const columns = queryAll<HTMLTableCellElement>(element, 'td');
+ assert.equal(columns[0].textContent, 'S');
+ });
+
+ test('shows M size', async () => {
+ element.change = {...change, insertions: 249, deletions: 0};
+ await element.updateComplete;
+
+ const columns = queryAll<HTMLSpanElement>(element, 'td');
+ assert.equal(columns[0].textContent, 'M');
+ });
+
+ test('shows L size', async () => {
+ element.change = {...change, insertions: 499, deletions: 500};
+ await element.updateComplete;
+
+ const columns = queryAll<HTMLTableCellElement>(element, 'td');
+ assert.equal(columns[0].textContent, 'L');
+ });
+
+ test('shows XL size', async () => {
+ element.change = {...change, insertions: 1000, deletions: 1};
+ await element.updateComplete;
+
+ const columns = queryAll<HTMLTableCellElement>(element, 'td');
+ assert.equal(columns[0].textContent, 'XL');
+ });
+});
diff --git a/polygerrit-ui/app/elements/topic/gr-topic-tree.ts b/polygerrit-ui/app/elements/topic/gr-topic-tree.ts
new file mode 100644
index 0000000..a993d90
--- /dev/null
+++ b/polygerrit-ui/app/elements/topic/gr-topic-tree.ts
@@ -0,0 +1,116 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './gr-topic-tree-repo';
+import {customElement, property, state} from 'lit/decorators';
+import {LitElement, html, PropertyValues} from 'lit-element/lit-element';
+import {getAppContext} from '../../services/app-context';
+import '../shared/gr-button/gr-button';
+import {ChangeInfo, RepoName} from '../../api/rest-api';
+
+/**
+ * A tree-like dashboard showing changes related to a topic, organized by
+ * repository.
+ */
+@customElement('gr-topic-tree')
+export class GrTopicTree extends LitElement {
+ @property({type: String})
+ topicName?: string;
+
+ @state()
+ private changesByRepo = new Map<RepoName, ChangeInfo[]>();
+
+ private restApiService = getAppContext().restApiService;
+
+ override willUpdate(changedProperties: PropertyValues) {
+ // TODO: Receive data from the model once it is added.
+ if (changedProperties.has('topicName')) {
+ this.loadAndSortChangesFromTopic();
+ }
+ }
+
+ override render() {
+ return html`
+ <table>
+ <thead>
+ <tr>
+ <td>Size</td>
+ <td>Subject</td>
+ <td>Topic</td>
+ <td>Branch</td>
+ <td>Owner</td>
+ <td>Status</td>
+ </tr>
+ </thead>
+ <tbody>
+ ${Array.from(this.changesByRepo).map(([repoName, changes]) =>
+ this.renderRepoSection(repoName, changes)
+ )}
+ </tbody>
+ </table>
+ `;
+ }
+
+ private renderRepoSection(repoName: RepoName, changes: ChangeInfo[]) {
+ return html`
+ <gr-topic-tree-repo
+ .repoName=${repoName}
+ .changes=${changes}
+ ></gr-topic-tree-repo>
+ `;
+ }
+
+ private async loadAndSortChangesFromTopic(): Promise<void> {
+ const changesInTopic = this.topicName
+ ? await this.restApiService.getChangesWithSameTopic(this.topicName)
+ : [];
+ const changesSubmittedTogether = await this.loadChangesSubmittedTogether(
+ changesInTopic
+ );
+ this.changesByRepo.clear();
+ for (const change of changesSubmittedTogether) {
+ if (this.changesByRepo.has(change.project)) {
+ this.changesByRepo.get(change.project)!.push(change);
+ } else {
+ this.changesByRepo.set(change.project, [change]);
+ }
+ }
+ this.requestUpdate();
+ }
+
+ private async loadChangesSubmittedTogether(
+ changesInTopic?: ChangeInfo[]
+ ): Promise<ChangeInfo[]> {
+ // All changes in the topic will be submitted together, so we can use any of
+ // them for the request to getChangesSubmittedTogether as long as the topic
+ // is not empty.
+ if (!changesInTopic || changesInTopic.length === 0) {
+ return [];
+ }
+ const response = await this.restApiService.getChangesSubmittedTogether(
+ changesInTopic[0]._number,
+ ['NON_VISIBLE_CHANGES', 'CURRENT_REVISION', 'CURRENT_COMMIT']
+ );
+ return response?.changes ?? [];
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-topic-tree': GrTopicTree;
+ }
+}
diff --git a/polygerrit-ui/app/elements/topic/gr-topic-tree_test.ts b/polygerrit-ui/app/elements/topic/gr-topic-tree_test.ts
new file mode 100644
index 0000000..72e14b8
--- /dev/null
+++ b/polygerrit-ui/app/elements/topic/gr-topic-tree_test.ts
@@ -0,0 +1,101 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {ChangeInfo, RepoName} from '../../api/rest-api';
+import '../../test/common-test-setup-karma';
+import {createChange} from '../../test/test-data-generators';
+import {mockPromise, queryAll, stubRestApi} from '../../test/test-utils';
+import {SubmittedTogetherInfo} from '../../types/common';
+import './gr-topic-tree';
+import {GrTopicTree} from './gr-topic-tree';
+import {GrTopicTreeRepo} from './gr-topic-tree-repo';
+
+const basicFixture = fixtureFromElement('gr-topic-tree');
+
+const repo1Name = 'repo1' as RepoName;
+const repo2Name = 'repo2' as RepoName;
+const repo3Name = 'repo3' as RepoName;
+
+function createChangeForRepo(repoName: string): ChangeInfo {
+ return {...createChange(), project: repoName as RepoName};
+}
+
+suite('gr-topic-tree tests', () => {
+ let element: GrTopicTree;
+ const repo1ChangeOutsideTopic = createChangeForRepo(repo1Name);
+ const repo1ChangesInTopic = [
+ createChangeForRepo(repo1Name),
+ createChangeForRepo(repo1Name),
+ ];
+ const repo2ChangesInTopic = [
+ createChangeForRepo(repo2Name),
+ createChangeForRepo(repo2Name),
+ ];
+ const repo3ChangesInTopic = [
+ createChangeForRepo(repo3Name),
+ createChangeForRepo(repo3Name),
+ ];
+
+ setup(async () => {
+ stubRestApi('getChangesWithSameTopic')
+ .withArgs('myTopic')
+ .resolves([
+ ...repo1ChangesInTopic,
+ ...repo2ChangesInTopic,
+ ...repo3ChangesInTopic,
+ ]);
+ const changesSubmittedTogetherPromise =
+ mockPromise<SubmittedTogetherInfo>();
+ stubRestApi('getChangesSubmittedTogether').returns(
+ changesSubmittedTogetherPromise
+ );
+ element = basicFixture.instantiate();
+ element.topicName = 'myTopic';
+
+ // The first update will trigger the data to be loaded. The second update
+ // will be rendering the loaded data.
+ await element.updateComplete;
+ changesSubmittedTogetherPromise.resolve({
+ changes: [
+ ...repo1ChangesInTopic,
+ repo1ChangeOutsideTopic,
+ ...repo2ChangesInTopic,
+ ...repo3ChangesInTopic,
+ ],
+ non_visible_changes: 0,
+ });
+ await changesSubmittedTogetherPromise;
+ await element.updateComplete;
+ });
+
+ test('groups changes by repo', () => {
+ const repoSections = queryAll<GrTopicTreeRepo>(
+ element,
+ 'gr-topic-tree-repo'
+ );
+ assert.lengthOf(repoSections, 3);
+ assert.equal(repoSections[0].repoName, repo1Name);
+ assert.sameMembers(repoSections[0].changes!, [
+ ...repo1ChangesInTopic,
+ repo1ChangeOutsideTopic,
+ ]);
+ assert.equal(repoSections[1].repoName, repo2Name);
+ assert.sameMembers(repoSections[1].changes!, repo2ChangesInTopic);
+ assert.equal(repoSections[2].repoName, repo3Name);
+ assert.sameMembers(repoSections[2].changes!, repo3ChangesInTopic);
+ });
+});
diff --git a/polygerrit-ui/app/elements/topic/gr-topic-view.ts b/polygerrit-ui/app/elements/topic/gr-topic-view.ts
new file mode 100644
index 0000000..86099b5
--- /dev/null
+++ b/polygerrit-ui/app/elements/topic/gr-topic-view.ts
@@ -0,0 +1,75 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {customElement, property, state} from 'lit/decorators';
+import {LitElement, html, PropertyValues} from 'lit';
+import {AppElementTopicParams} from '../gr-app-types';
+import {getAppContext} from '../../services/app-context';
+import {KnownExperimentId} from '../../services/flags/flags';
+import {GerritNav} from '../core/gr-navigation/gr-navigation';
+import {GerritView} from '../../services/router/router-model';
+import './gr-topic-summary';
+import './gr-topic-tree';
+
+/**
+ * A page showing all information about a topic and changes that are related
+ * to that topic.
+ */
+@customElement('gr-topic-view')
+export class GrTopicView extends LitElement {
+ @property({type: Object})
+ params?: AppElementTopicParams;
+
+ @state()
+ topicName?: string;
+
+ private readonly flagsService = getAppContext().flagsService;
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('params')) {
+ this.paramsChanged();
+ }
+ }
+
+ override render() {
+ if (this.topicName === undefined) {
+ return;
+ }
+ // TODO: Add topic selector
+ return html`
+ <gr-topic-summary .topicName=${this.topicName}></gr-topic-summary>
+ <gr-topic-tree .topicName=${this.topicName}></gr-topic-tree>
+ `;
+ }
+
+ paramsChanged() {
+ if (this.params?.view !== GerritView.TOPIC) return;
+ this.topicName = this.params?.topic;
+ if (
+ !this.flagsService.isEnabled(KnownExperimentId.TOPICS_PAGE) &&
+ this.topicName
+ ) {
+ GerritNav.navigateToSearchQuery(`topic:${this.topicName}`);
+ }
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-topic-view': GrTopicView;
+ }
+}
diff --git a/polygerrit-ui/app/elements/topic/gr-topic-view_test.ts b/polygerrit-ui/app/elements/topic/gr-topic-view_test.ts
new file mode 100644
index 0000000..726fca2
--- /dev/null
+++ b/polygerrit-ui/app/elements/topic/gr-topic-view_test.ts
@@ -0,0 +1,59 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {KnownExperimentId} from '../../services/flags/flags';
+import '../../test/common-test-setup-karma';
+import {createGenerateUrlTopicViewParams} from '../../test/test-data-generators';
+import {stubFlags} from '../../test/test-utils';
+import {GerritNav} from '../core/gr-navigation/gr-navigation';
+import './gr-topic-view';
+import {GrTopicView} from './gr-topic-view';
+
+const basicFixture = fixtureFromElement('gr-topic-view');
+
+suite('gr-topic-view tests', () => {
+ let element: GrTopicView;
+ let redirectStub: sinon.SinonStub;
+
+ async function commonSetup(experimentEnabled: boolean) {
+ redirectStub = sinon.stub(GerritNav, 'navigateToSearchQuery');
+ stubFlags('isEnabled')
+ .withArgs(KnownExperimentId.TOPICS_PAGE)
+ .returns(experimentEnabled);
+ element = basicFixture.instantiate();
+ element.params = createGenerateUrlTopicViewParams();
+ await element.updateComplete;
+ }
+
+ suite('experiment enabled', () => {
+ setup(async () => {
+ await commonSetup(true);
+ });
+ test('does not redirect to search results page if experiment is enabled', () => {
+ assert.isTrue(redirectStub.notCalled);
+ });
+ });
+
+ suite('experiment disabled', () => {
+ setup(async () => {
+ await commonSetup(false);
+ });
+ test('redirects to search results page if experiment is disabled', () => {
+ assert.isTrue(redirectStub.calledWith('topic:myTopic'));
+ });
+ });
+});
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 6a30477..bbcfd23 100644
--- a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
@@ -15,16 +15,19 @@
* limitations under the License.
*/
-import {appContext} from '../services/app-context';
+import {create, Registry, Finalizable} from '../services/registry';
+import {AppContext} from '../services/app-context';
+import {AuthService} from '../services/gr-auth/gr-auth';
import {FlagsService} from '../services/flags/flags';
import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock';
-import {AuthService} from '../services/gr-auth/gr-auth';
class MockFlagsService implements FlagsService {
isEnabled() {
return false;
}
+ finalize() {}
+
/**
* @returns array of all enabled experiments.
*/
@@ -48,6 +51,8 @@
setup() {}
+ finalize() {}
+
fetch() {
const blob = new Blob();
const init = {status: 200, statusText: 'Ack'};
@@ -59,15 +64,38 @@
// Setup mocks for appContext.
// This is a temporary solution
// TODO(dmfilippov): find a better solution for gr-diff
-export function initDiffAppContext() {
- function setMock(serviceName: string, setupMock: unknown) {
- Object.defineProperty(appContext, serviceName, {
- get() {
- return setupMock;
- },
- });
- }
- setMock('flagsService', new MockFlagsService());
- setMock('reportingService', grReportingMock);
- setMock('authService', new MockAuthService());
+export function createDiffAppContext(): AppContext & Finalizable {
+ const appRegistry: Registry<AppContext> = {
+ flagsService: (_ctx: Partial<AppContext>) => new MockFlagsService(),
+ authService: (_ctx: Partial<AppContext>) => new MockAuthService(),
+ reportingService: (_ctx: Partial<AppContext>) => grReportingMock,
+ eventEmitter: (_ctx: Partial<AppContext>) => {
+ throw new Error('eventEmitter is not implemented');
+ },
+ restApiService: (_ctx: Partial<AppContext>) => {
+ throw new Error('restApiService is not implemented');
+ },
+ changeModel: (_ctx: Partial<AppContext>) => {
+ throw new Error('changeModel is not implemented');
+ },
+ checksModel: (_ctx: Partial<AppContext>) => {
+ throw new Error('checksModel 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');
+ },
+ routerModel: (_ctx: Partial<AppContext>) => {
+ throw new Error('routerModel is not implemented');
+ },
+ shortcutsService: (_ctx: Partial<AppContext>) => {
+ throw new Error('shortcutsService is not implemented');
+ },
+ };
+ return create<AppContext>(appRegistry);
}
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init_test.js b/polygerrit-ui/app/embed/gr-diff-app-context-init_test.js
index 832c931..bb46484 100644
--- a/polygerrit-ui/app/embed/gr-diff-app-context-init_test.js
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init_test.js
@@ -16,14 +16,11 @@
*/
import '../test/common-test-setup-karma.js';
-import {appContext} from '../services/app-context.js';
-import {initDiffAppContext} from './gr-diff-app-context-init.js';
-suite('gr diff app context initializer tests', () => {
- setup(() => {
- initDiffAppContext();
- });
+import {createDiffAppContext} from './gr-diff-app-context-init.js';
+suite('gr diff app context initializer tests', () => {
test('all services initialized and are singletons', () => {
+ const appContext = createDiffAppContext();
Object.keys(appContext).forEach(serviceName => {
const service = appContext[serviceName];
assert.isNotNull(service);
diff --git a/polygerrit-ui/app/embed/gr-diff.ts b/polygerrit-ui/app/embed/gr-diff.ts
index 422667a4..64ef214 100644
--- a/polygerrit-ui/app/embed/gr-diff.ts
+++ b/polygerrit-ui/app/embed/gr-diff.ts
@@ -28,11 +28,12 @@
import {TokenHighlightLayer} from '../elements/diff/gr-diff-builder/token-highlight-layer';
import {GrDiffCursor} from '../elements/diff/gr-diff-cursor/gr-diff-cursor';
import {GrAnnotation} from '../elements/diff/gr-diff-highlight/gr-annotation';
-import {initDiffAppContext} from './gr-diff-app-context-init';
+import {createDiffAppContext} from './gr-diff-app-context-init';
+import {injectAppContext} from '../services/app-context';
// Setup appContext for diff.
// TODO (dmfilippov): find a better solution
-initDiffAppContext();
+injectAppContext(createDiffAppContext());
// Setup global variables for existing usages of this component
window.grdiff = {
GrAnnotation,
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 dd86b38..e6b63e6 100644
--- a/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
+++ b/polygerrit-ui/app/mixins/hovercard-mixin/hovercard-mixin_test.ts
@@ -47,7 +47,7 @@
let element: HovercardMixinTest;
let button: HTMLElement;
- let testPromise: MockPromise;
+ let testPromise: MockPromise<void>;
setup(() => {
testPromise = mockPromise();
diff --git a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
index f133c116..8e27c74 100644
--- a/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
+++ b/polygerrit-ui/app/mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.ts
@@ -17,7 +17,7 @@
import {property} from '@polymer/decorators';
import {PolymerElement} from '@polymer/polymer';
import {check, Constructor} from '../../utils/common-util';
-import {appContext} from '../../services/app-context';
+import {getAppContext} from '../../services/app-context';
import {
Shortcut,
ShortcutSection,
@@ -50,7 +50,7 @@
// This enables `ShortcutSection` to be used in the html template.
ShortcutSection = ShortcutSection;
- private readonly shortcuts = appContext.shortcutsService;
+ private readonly shortcuts = getAppContext().shortcutsService;
/** Used to disable shortcuts when the element is not visible. */
private observer?: IntersectionObserver;
diff --git a/polygerrit-ui/app/models/browser/browser-model.ts b/polygerrit-ui/app/models/browser/browser-model.ts
new file mode 100644
index 0000000..490f868
--- /dev/null
+++ b/polygerrit-ui/app/models/browser/browser-model.ts
@@ -0,0 +1,82 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Observable, combineLatest} from 'rxjs';
+import {distinctUntilChanged, map} from 'rxjs/operators';
+import {Finalizable} from '../../services/registry';
+import {define} from '../dependency';
+import {DiffViewMode} from '../../api/diff';
+import {UserModel} from '../user/user-model';
+import {Model} from '../model';
+
+// This value is somewhat arbitrary and not based on research or calculations.
+const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 850;
+
+export interface BrowserState {
+ /**
+ * We maintain the screen width in the state so that the app can react to
+ * changes in the width such as automatically changing to unified diff view
+ */
+ screenWidth?: number;
+}
+
+const initialState: BrowserState = {};
+
+export const browserModelToken = define<BrowserModel>('browser-model');
+
+export class BrowserModel extends Model<BrowserState> implements Finalizable {
+ readonly diffViewMode$: Observable<DiffViewMode>;
+
+ constructor(readonly userModel: UserModel) {
+ super(initialState);
+ const screenWidth$ = this.state$.pipe(
+ map(
+ state =>
+ !!state.screenWidth &&
+ state.screenWidth < MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX
+ ),
+ distinctUntilChanged()
+ );
+ // TODO; Inject the UserModel once preferenceDiffViewMode$ has moved to
+ // the user model.
+ this.diffViewMode$ = combineLatest([
+ screenWidth$,
+ userModel.preferenceDiffViewMode$,
+ ]).pipe(
+ map(([isScreenTooSmall, preferenceDiffViewMode]) => {
+ if (isScreenTooSmall) return DiffViewMode.UNIFIED;
+ else return preferenceDiffViewMode;
+ }),
+ distinctUntilChanged()
+ );
+ }
+
+ /* Observe the screen width so that the app can react to changes to it */
+ observeWidth() {
+ return new ResizeObserver(entries => {
+ entries.forEach(entry => {
+ this.setScreenWidth(entry.contentRect.width);
+ });
+ });
+ }
+
+ // Private but used in tests.
+ setScreenWidth(screenWidth: number) {
+ this.subject$.next({...this.subject$.getValue(), screenWidth});
+ }
+
+ finalize() {}
+}
diff --git a/polygerrit-ui/app/models/comments/comments-model.ts b/polygerrit-ui/app/models/comments/comments-model.ts
new file mode 100644
index 0000000..a722e14
--- /dev/null
+++ b/polygerrit-ui/app/models/comments/comments-model.ts
@@ -0,0 +1,555 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {ChangeComments} from '../../elements/diff/gr-comment-api/gr-comment-api';
+import {
+ CommentBasics,
+ CommentInfo,
+ NumericChangeId,
+ PatchSetNum,
+ RevisionId,
+ UrlEncodedCommentId,
+ PathToCommentsInfoMap,
+ RobotCommentInfo,
+} from '../../types/common';
+import {
+ addPath,
+ DraftInfo,
+ isDraft,
+ isUnsaved,
+ reportingDetails,
+ UnsavedInfo,
+} from '../../utils/comment-util';
+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, Subscription} from 'rxjs';
+import {fire, fireAlert, fireEvent} from '../../utils/event-util';
+import {CURRENT} from '../../utils/patch-set-util';
+import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
+import {ChangeModel} from '../../services/change/change-model';
+import {Interaction, Timing} from '../../constants/reporting';
+import {assertIsDefined} from '../../utils/common-util';
+import {debounce, DelayedTask} from '../../utils/async-util';
+import {pluralize} from '../../utils/string-util';
+import {ReportingService} from '../../services/gr-reporting/gr-reporting';
+import {Model} from '../model';
+
+export interface CommentState {
+ /** undefined means 'still loading' */
+ comments?: PathToCommentsInfoMap;
+ /** undefined means 'still loading' */
+ robotComments?: {[path: string]: RobotCommentInfo[]};
+ // All drafts are DraftInfo objects and have __draft = true set.
+ // Drafts have an id and are known to the backend. Unsaved drafts
+ // (see UnsavedInfo) do NOT belong in the application model.
+ /** undefined means 'still loading' */
+ drafts?: {[path: string]: DraftInfo[]};
+ // Ported comments only affect `CommentThread` properties, not individual
+ // comments.
+ /** undefined means 'still loading' */
+ portedComments?: PathToCommentsInfoMap;
+ /** undefined means 'still loading' */
+ portedDrafts?: PathToCommentsInfoMap;
+ /**
+ * If a draft is discarded by the user, then we temporarily keep it in this
+ * array in case the user decides to Undo the discard operation and bring the
+ * draft back. Once restored, the draft is removed from this array.
+ */
+ discardedDrafts: DraftInfo[];
+}
+
+const initialState: CommentState = {
+ comments: undefined,
+ robotComments: undefined,
+ drafts: undefined,
+ portedComments: undefined,
+ portedDrafts: undefined,
+ discardedDrafts: [],
+};
+
+const TOAST_DEBOUNCE_INTERVAL = 200;
+
+function getSavingMessage(numPending: number, requestFailed?: boolean) {
+ if (requestFailed) {
+ return 'Unable to save draft';
+ }
+ if (numPending === 0) {
+ return 'All changes saved';
+ }
+ return `Saving ${pluralize(numPending, 'draft')}...`;
+}
+
+// Private but used in tests.
+export function setComments(
+ state: CommentState,
+ comments?: {
+ [path: string]: CommentInfo[];
+ }
+): CommentState {
+ const nextState = {...state};
+ if (deepEqual(comments, nextState.comments)) return state;
+ nextState.comments = addPath(comments) || {};
+ return nextState;
+}
+
+// Private but used in tests.
+export function setRobotComments(
+ state: CommentState,
+ robotComments?: {
+ [path: string]: RobotCommentInfo[];
+ }
+): CommentState {
+ if (deepEqual(robotComments, state.robotComments)) return state;
+ const nextState = {...state};
+ nextState.robotComments = addPath(robotComments) || {};
+ return nextState;
+}
+
+// Private but used in tests.
+export function setDrafts(
+ state: CommentState,
+ drafts?: {[path: string]: DraftInfo[]}
+): CommentState {
+ if (deepEqual(drafts, state.drafts)) return state;
+ const nextState = {...state};
+ nextState.drafts = addPath(drafts);
+ return nextState;
+}
+
+// Private but used in tests.
+export function setPortedComments(
+ state: CommentState,
+ portedComments?: PathToCommentsInfoMap
+): CommentState {
+ if (deepEqual(portedComments, state.portedComments)) return state;
+ const nextState = {...state};
+ nextState.portedComments = portedComments || {};
+ return nextState;
+}
+
+// Private but used in tests.
+export function setPortedDrafts(
+ state: CommentState,
+ portedDrafts?: PathToCommentsInfoMap
+): CommentState {
+ if (deepEqual(portedDrafts, state.portedDrafts)) return state;
+ const nextState = {...state};
+ nextState.portedDrafts = portedDrafts || {};
+ return nextState;
+}
+
+// Private but used in tests.
+export function setDiscardedDraft(
+ state: CommentState,
+ draft: DraftInfo
+): CommentState {
+ const nextState = {...state};
+ nextState.discardedDrafts = [...nextState.discardedDrafts, draft];
+ return nextState;
+}
+
+// Private but used in tests.
+export function deleteDiscardedDraft(
+ state: CommentState,
+ draftID?: string
+): CommentState {
+ const nextState = {...state};
+ const drafts = [...nextState.discardedDrafts];
+ const index = drafts.findIndex(d => d.id === draftID);
+ if (index === -1) {
+ throw new Error('discarded draft not found');
+ }
+ drafts.splice(index, 1);
+ nextState.discardedDrafts = drafts;
+ return nextState;
+}
+
+/** Adds or updates a draft. */
+export function setDraft(state: CommentState, draft: DraftInfo): CommentState {
+ const nextState = {...state};
+ if (!draft.path) throw new Error('draft path undefined');
+ if (!isDraft(draft)) throw new Error('draft is not a draft');
+ if (isUnsaved(draft)) throw new Error('unsaved drafts dont belong to model');
+
+ nextState.drafts = {...nextState.drafts};
+ const drafts = nextState.drafts;
+ if (!drafts[draft.path]) drafts[draft.path] = [] as DraftInfo[];
+ else drafts[draft.path] = [...drafts[draft.path]];
+ const index = drafts[draft.path].findIndex(d => d.id && d.id === draft.id);
+ if (index !== -1) {
+ drafts[draft.path][index] = draft;
+ } else {
+ drafts[draft.path].push(draft);
+ }
+ return nextState;
+}
+
+export function deleteDraft(
+ state: CommentState,
+ draft: DraftInfo
+): CommentState {
+ const nextState = {...state};
+ if (!draft.path) throw new Error('draft path undefined');
+ if (!isDraft(draft)) throw new Error('draft is not a draft');
+ if (isUnsaved(draft)) throw new Error('unsaved drafts dont belong to model');
+ nextState.drafts = {...nextState.drafts};
+ const drafts = nextState.drafts;
+ const index = (drafts[draft.path] || []).findIndex(
+ d => d.id && d.id === draft.id
+ );
+ if (index === -1) return state;
+ const discardedDraft = drafts[draft.path][index];
+ drafts[draft.path] = [...drafts[draft.path]];
+ drafts[draft.path].splice(index, 1);
+ return setDiscardedDraft(nextState, discardedDraft);
+}
+
+export const commentsModelToken = define<CommentsModel>('comments-model');
+export class CommentsModel extends Model<CommentState> implements Finalizable {
+ public readonly commentsLoading$ = select(
+ this.state$,
+ commentState =>
+ commentState.comments === undefined ||
+ commentState.robotComments === undefined ||
+ commentState.drafts === undefined
+ );
+
+ public readonly comments$ = select(
+ this.state$,
+ commentState => commentState.comments
+ );
+
+ public readonly drafts$ = select(
+ this.state$,
+ commentState => commentState.drafts
+ );
+
+ public readonly portedComments$ = select(
+ this.state$,
+ commentState => commentState.portedComments
+ );
+
+ public readonly discardedDrafts$ = select(
+ this.state$,
+ commentState => commentState.discardedDrafts
+ );
+
+ // Emits a new value even if only a single draft is changed. Components should
+ // aim to subsribe to something more specific.
+ public readonly changeComments$ = select(
+ this.state$,
+ commentState =>
+ new ChangeComments(
+ commentState.comments,
+ commentState.robotComments,
+ commentState.drafts,
+ commentState.portedComments,
+ commentState.portedDrafts
+ )
+ );
+
+ public readonly threads$ = select(this.changeComments$, changeComments =>
+ changeComments.getAllThreadsForChange()
+ );
+
+ public thread$(id: UrlEncodedCommentId) {
+ return select(this.threads$, threads => threads.find(t => t.rootId === id));
+ }
+
+ private numPendingDraftRequests = 0;
+
+ private changeNum?: NumericChangeId;
+
+ private patchNum?: PatchSetNum;
+
+ private readonly reloadListener: () => void;
+
+ private readonly subscriptions: Subscription[] = [];
+
+ private drafts: {[path: string]: DraftInfo[]} = {};
+
+ private draftToastTask?: DelayedTask;
+
+ private discardedDrafts: DraftInfo[] = [];
+
+ constructor(
+ readonly routerModel: RouterModel,
+ readonly changeModel: ChangeModel,
+ readonly restApiService: RestApiService,
+ readonly reporting: ReportingService
+ ) {
+ super(initialState);
+ this.subscriptions.push(
+ this.discardedDrafts$.subscribe(x => (this.discardedDrafts = x))
+ );
+ this.subscriptions.push(
+ this.drafts$.subscribe(x => (this.drafts = x ?? {}))
+ );
+ this.subscriptions.push(
+ this.changeModel.currentPatchNum$.subscribe(x => (this.patchNum = x))
+ );
+ this.subscriptions.push(
+ this.routerModel.routerChangeNum$.subscribe(changeNum => {
+ this.changeNum = changeNum;
+ this.setState({...initialState});
+ this.reloadAllComments();
+ })
+ );
+ this.subscriptions.push(
+ combineLatest([
+ this.changeModel.changeNum$,
+ this.changeModel.currentPatchNum$,
+ ]).subscribe(([changeNum, patchNum]) => {
+ this.changeNum = changeNum;
+ this.patchNum = patchNum;
+ this.reloadAllPortedComments();
+ })
+ );
+ this.reloadListener = () => {
+ this.reloadAllComments();
+ this.reloadAllPortedComments();
+ };
+ document.addEventListener('reload', this.reloadListener);
+ }
+
+ finalize() {
+ document.removeEventListener('reload', this.reloadListener!);
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions.splice(0, this.subscriptions.length);
+ }
+
+ // Note that this does *not* reload ported comments.
+ async reloadAllComments() {
+ if (!this.changeNum) return;
+ await Promise.all([
+ this.reloadComments(this.changeNum),
+ this.reloadRobotComments(this.changeNum),
+ this.reloadDrafts(this.changeNum),
+ ]);
+ }
+
+ async reloadAllPortedComments() {
+ if (!this.changeNum) return;
+ if (!this.patchNum) return;
+ await Promise.all([
+ this.reloadPortedComments(this.changeNum, this.patchNum),
+ this.reloadPortedDrafts(this.changeNum, this.patchNum),
+ ]);
+ }
+
+ // visible for testing
+ updateState(reducer: (state: CommentState) => CommentState) {
+ const current = this.subject$.getValue();
+ this.setState(reducer({...current}));
+ }
+
+ // visible for testing
+ setState(state: CommentState) {
+ this.subject$.next(state);
+ }
+
+ async reloadComments(changeNum: NumericChangeId): Promise<void> {
+ const comments = await this.restApiService.getDiffComments(changeNum);
+ this.updateState(s => setComments(s, comments));
+ }
+
+ async reloadRobotComments(changeNum: NumericChangeId): Promise<void> {
+ const robotComments = await this.restApiService.getDiffRobotComments(
+ changeNum
+ );
+ this.updateState(s => setRobotComments(s, robotComments));
+ }
+
+ async reloadDrafts(changeNum: NumericChangeId): Promise<void> {
+ const drafts = await this.restApiService.getDiffDrafts(changeNum);
+ this.updateState(s => setDrafts(s, drafts));
+ }
+
+ async reloadPortedComments(
+ changeNum: NumericChangeId,
+ patchNum = CURRENT as RevisionId
+ ): Promise<void> {
+ const portedComments = await this.restApiService.getPortedComments(
+ changeNum,
+ patchNum
+ );
+ this.updateState(s => setPortedComments(s, portedComments));
+ }
+
+ async reloadPortedDrafts(
+ changeNum: NumericChangeId,
+ patchNum = CURRENT as RevisionId
+ ): Promise<void> {
+ const portedDrafts = await this.restApiService.getPortedDrafts(
+ changeNum,
+ patchNum
+ );
+ this.updateState(s => setPortedDrafts(s, portedDrafts));
+ }
+
+ async restoreDraft(id: UrlEncodedCommentId) {
+ const found = this.discardedDrafts?.find(d => d.id === id);
+ if (!found) throw new Error('discarded draft not found');
+ const newDraft = {
+ ...found,
+ id: undefined,
+ updated: undefined,
+ __draft: undefined,
+ __unsaved: true,
+ };
+ await this.saveDraft(newDraft);
+ this.updateState(s => deleteDiscardedDraft(s, id));
+ }
+
+ /**
+ * Saves a new or updates an existing draft.
+ * The model will only be updated when a successful response comes back.
+ */
+ async saveDraft(
+ draft: DraftInfo | UnsavedInfo,
+ showToast = true
+ ): Promise<DraftInfo> {
+ assertIsDefined(this.changeNum, 'change number');
+ assertIsDefined(draft.patch_set, 'patchset number of comment draft');
+ if (!draft.message?.trim()) throw new Error('Cannot save empty draft.');
+
+ // Saving the change number as to make sure that the response is still
+ // relevant when it comes back. The user maybe have navigated away.
+ const changeNum = this.changeNum;
+ this.report(Interaction.SAVE_COMMENT, draft);
+ if (showToast) this.showStartRequest();
+ const timing = isUnsaved(draft) ? Timing.DRAFT_CREATE : Timing.DRAFT_UPDATE;
+ const timer = this.reporting.getTimer(timing);
+ const result = await this.restApiService.saveDiffDraft(
+ changeNum,
+ draft.patch_set,
+ draft
+ );
+ if (changeNum !== this.changeNum) throw new Error('change changed');
+ if (!result.ok) {
+ if (showToast) this.handleFailedDraftRequest();
+ throw new Error(
+ `Failed to save draft comment: ${JSON.stringify(result)}`
+ );
+ }
+ const obj = await this.restApiService.getResponseObject(result);
+ const savedComment = obj as unknown as CommentInfo;
+ const updatedDraft = {
+ ...draft,
+ id: savedComment.id,
+ updated: savedComment.updated,
+ __draft: true,
+ __unsaved: undefined,
+ };
+ timer.end({id: updatedDraft.id});
+ if (showToast) this.showEndRequest();
+ this.updateState(s => setDraft(s, updatedDraft));
+ this.report(Interaction.COMMENT_SAVED, updatedDraft);
+ return updatedDraft;
+ }
+
+ async discardDraft(draftId: UrlEncodedCommentId) {
+ const draft = this.lookupDraft(draftId);
+ assertIsDefined(this.changeNum, 'change number');
+ assertIsDefined(draft, `draft not found by id ${draftId}`);
+ assertIsDefined(draft.patch_set, 'patchset number of comment draft');
+
+ if (!draft.message?.trim()) throw new Error('saved draft cant be empty');
+ // Saving the change number as to make sure that the response is still
+ // relevant when it comes back. The user maybe have navigated away.
+ const changeNum = this.changeNum;
+ this.report(Interaction.DISCARD_COMMENT, draft);
+ this.showStartRequest();
+ const timer = this.reporting.getTimer(Timing.DRAFT_DISCARD);
+ const result = await this.restApiService.deleteDiffDraft(
+ changeNum,
+ draft.patch_set,
+ {id: draft.id}
+ );
+ timer.end({id: draft.id});
+ if (changeNum !== this.changeNum) throw new Error('change changed');
+ if (!result.ok) {
+ this.handleFailedDraftRequest();
+ throw new Error(
+ `Failed to discard draft comment: ${JSON.stringify(result)}`
+ );
+ }
+ this.showEndRequest();
+ this.updateState(s => deleteDraft(s, draft));
+ // We don't store empty discarded drafts and don't need an UNDO then.
+ if (draft.message?.trim()) {
+ fire(document, 'show-alert', {
+ message: 'Draft Discarded',
+ action: 'Undo',
+ callback: () => this.restoreDraft(draft.id),
+ });
+ }
+ this.report(Interaction.COMMENT_DISCARDED, draft);
+ }
+
+ private report(interaction: Interaction, comment: CommentBasics) {
+ const details = reportingDetails(comment);
+ this.reporting.reportInteraction(interaction, details);
+ }
+
+ private showStartRequest() {
+ this.numPendingDraftRequests += 1;
+ this.updateRequestToast();
+ }
+
+ private showEndRequest() {
+ this.numPendingDraftRequests -= 1;
+ this.updateRequestToast();
+ }
+
+ private handleFailedDraftRequest() {
+ this.numPendingDraftRequests -= 1;
+ this.updateRequestToast(/* requestFailed=*/ true);
+ }
+
+ private updateRequestToast(requestFailed?: boolean) {
+ if (this.numPendingDraftRequests === 0 && !requestFailed) {
+ fireEvent(document, 'hide-alert');
+ return;
+ }
+ const message = getSavingMessage(
+ this.numPendingDraftRequests,
+ requestFailed
+ );
+ this.draftToastTask = debounce(
+ this.draftToastTask,
+ () => {
+ // Note: the event is fired on the body rather than this element because
+ // this element may not be attached by the time this executes, in which
+ // case the event would not bubble.
+ fireAlert(document.body, message);
+ },
+ TOAST_DEBOUNCE_INTERVAL
+ );
+ }
+
+ private lookupDraft(id: UrlEncodedCommentId): DraftInfo | undefined {
+ return Object.values(this.drafts)
+ .flat()
+ .find(d => d.id === id);
+ }
+}
diff --git a/polygerrit-ui/app/models/comments/comments-model_test.ts b/polygerrit-ui/app/models/comments/comments-model_test.ts
new file mode 100644
index 0000000..e713893
--- /dev/null
+++ b/polygerrit-ui/app/models/comments/comments-model_test.ts
@@ -0,0 +1,128 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../../test/common-test-setup-karma';
+import {createDraft} from '../../test/test-data-generators';
+import {UrlEncodedCommentId} from '../../types/common';
+import {DraftInfo} from '../../utils/comment-util';
+import './comments-model';
+import {CommentsModel} from './comments-model';
+import {deleteDraft} from './comments-model';
+import {Subscription} from 'rxjs';
+import '../../test/common-test-setup-karma';
+import {
+ createComment,
+ createParsedChange,
+ TEST_NUMERIC_CHANGE_ID,
+} from '../../test/test-data-generators';
+import {stubRestApi, waitUntil, waitUntilCalled} from '../../test/test-utils';
+import {getAppContext} from '../../services/app-context';
+import {GerritView} from '../../services/router/router-model';
+import {PathToCommentsInfoMap} from '../../types/common';
+
+suite('comments model tests', () => {
+ test('updateStateDeleteDraft', () => {
+ const draft = createDraft();
+ draft.id = '1' as UrlEncodedCommentId;
+ const state = {
+ comments: {},
+ robotComments: {},
+ drafts: {
+ [draft.path!]: [draft as DraftInfo],
+ },
+ portedComments: {},
+ portedDrafts: {},
+ discardedDrafts: [],
+ };
+ const output = deleteDraft(state, draft);
+ assert.deepEqual(output, {
+ comments: {},
+ robotComments: {},
+ drafts: {
+ 'abc.txt': [],
+ },
+ portedComments: {},
+ portedDrafts: {},
+ discardedDrafts: [{...draft}],
+ });
+ });
+});
+
+suite('change service tests', () => {
+ let subscriptions: Subscription[] = [];
+
+ teardown(() => {
+ for (const s of subscriptions) {
+ s.unsubscribe();
+ }
+ subscriptions = [];
+ });
+
+ test('loads comments', async () => {
+ const model = new CommentsModel(
+ getAppContext().routerModel,
+ getAppContext().changeModel,
+ getAppContext().restApiService,
+ getAppContext().reportingService
+ );
+ const diffCommentsSpy = stubRestApi('getDiffComments').returns(
+ Promise.resolve({'foo.c': [createComment()]})
+ );
+ const diffRobotCommentsSpy = stubRestApi('getDiffRobotComments').returns(
+ Promise.resolve({})
+ );
+ const diffDraftsSpy = stubRestApi('getDiffDrafts').returns(
+ Promise.resolve({})
+ );
+ const portedCommentsSpy = stubRestApi('getPortedComments').returns(
+ Promise.resolve({'foo.c': [createComment()]})
+ );
+ const portedDraftsSpy = stubRestApi('getPortedDrafts').returns(
+ Promise.resolve({})
+ );
+ let comments: PathToCommentsInfoMap = {};
+ subscriptions.push(model.comments$.subscribe(c => (comments = c ?? {})));
+ let portedComments: PathToCommentsInfoMap = {};
+ subscriptions.push(
+ model.portedComments$.subscribe(c => (portedComments = c ?? {}))
+ );
+
+ model.routerModel.updateState({
+ view: GerritView.CHANGE,
+ changeNum: TEST_NUMERIC_CHANGE_ID,
+ });
+ model.changeModel.updateStateChange(createParsedChange());
+
+ await waitUntilCalled(diffCommentsSpy, 'diffCommentsSpy');
+ await waitUntilCalled(diffRobotCommentsSpy, 'diffRobotCommentsSpy');
+ await waitUntilCalled(diffDraftsSpy, 'diffDraftsSpy');
+ await waitUntilCalled(portedCommentsSpy, 'portedCommentsSpy');
+ await waitUntilCalled(portedDraftsSpy, 'portedDraftsSpy');
+ await waitUntil(
+ () => Object.keys(comments).length > 0,
+ 'comment in model not set'
+ );
+ await waitUntil(
+ () => Object.keys(portedComments).length > 0,
+ 'ported comment in model not set'
+ );
+
+ assert.equal(comments['foo.c'].length, 1);
+ assert.equal(comments['foo.c'][0].id, '12345');
+ assert.equal(portedComments['foo.c'].length, 1);
+ assert.equal(portedComments['foo.c'][0].id, '12345');
+ });
+});
diff --git a/polygerrit-ui/app/models/config/config-model.ts b/polygerrit-ui/app/models/config/config-model.ts
new file mode 100644
index 0000000..e57a724
--- /dev/null
+++ b/polygerrit-ui/app/models/config/config-model.ts
@@ -0,0 +1,89 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {ConfigInfo, RepoName, ServerInfo} from '../../types/common';
+import {from, of, Subscription} 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 '../../services/change/change-model';
+import {select} from '../../utils/observable-util';
+import {Model} from '../model';
+import {define} from '../dependency';
+
+export interface ConfigState {
+ repoConfig?: ConfigInfo;
+ serverConfig?: ServerInfo;
+}
+
+export const configModelToken = define<ConfigModel>('config-model');
+export class ConfigModel extends Model<ConfigState> implements Finalizable {
+ public repoConfig$ = select(
+ this.state$,
+ configState => configState.repoConfig
+ );
+
+ public repoCommentLinks$ = select(
+ this.repoConfig$,
+ repoConfig => repoConfig?.commentlinks ?? {}
+ );
+
+ public serverConfig$ = select(
+ this.state$,
+ configState => configState.serverConfig
+ );
+
+ private subscriptions: Subscription[];
+
+ constructor(
+ readonly changeModel: ChangeModel,
+ readonly restApiService: RestApiService
+ ) {
+ super({});
+ this.subscriptions = [
+ from(this.restApiService.getConfig()).subscribe((config?: ServerInfo) => {
+ this.updateServerConfig(config);
+ }),
+ this.changeModel.repo$
+ .pipe(
+ switchMap((repo?: RepoName) => {
+ if (repo === undefined) return of(undefined);
+ return from(this.restApiService.getProjectConfig(repo));
+ })
+ )
+ .subscribe((repoConfig?: ConfigInfo) => {
+ this.updateRepoConfig(repoConfig);
+ }),
+ ];
+ }
+
+ updateRepoConfig(repoConfig?: ConfigInfo) {
+ const current = this.subject$.getValue();
+ this.subject$.next({...current, repoConfig});
+ }
+
+ updateServerConfig(serverConfig?: ServerInfo) {
+ const current = this.subject$.getValue();
+ this.subject$.next({...current, serverConfig});
+ }
+
+ finalize() {
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions = [];
+ }
+}
diff --git a/polygerrit-ui/app/models/dependency.ts b/polygerrit-ui/app/models/dependency.ts
new file mode 100644
index 0000000..e7ac242c
--- /dev/null
+++ b/polygerrit-ui/app/models/dependency.ts
@@ -0,0 +1,332 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {ReactiveController, ReactiveControllerHost} from 'lit';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+
+/**
+ * This module provides the ability to do dependency injection in components.
+ * It provides 3 functions that are for the purpose of dependency injection.
+ *
+ * Definitions
+ * ---
+ * A component's "connected lifetime" consists of the span between
+ * `super.connectedCallback` and `super.disconnectedCallback`.
+ *
+ * Dependency Definition
+ * ---
+ *
+ * A token for a dependency of type FooService is defined as follows:
+ *
+ * const fooToken = define<FooService>('some name');
+ *
+ * Dependency Resolution
+ * ---
+ *
+ * To get the value of a dependency, a component requests a resolved dependency
+ *
+ * ```
+ * private readonly serviceRef = resolve(this, fooToken);
+ * ```
+ *
+ * A resolved dependency is a function that when called will return the actual
+ * value for that dependency.
+ *
+ * A resolved dependency is guaranteed to be resolved during a components
+ * connected lifetime. If no ancestor provided a value for the dependency, then
+ * the resolved dependency will throw an error if the value is accessed.
+ * Therefore, the following is safe-by-construction as long as it happens
+ * within a components connected lifetime:
+ *
+ * ```
+ * serviceRef().fooServiceMethod()
+ * ```
+ *
+ * Dependency Injection
+ * ---
+ *
+ * Ancestor components will inject the dependencies that a child component
+ * requires by providing factories for those values.
+ *
+ *
+ * To provide a dependency, a component needs to specify the following prior
+ * to finishing its connectedCallback:
+ *
+ * ```
+ * provide(this, fooToken, () => new FooImpl())
+ * ```
+ * Dependencies are injected as factories in case the construction of them
+ * depends on other dependencies further up the component chain. For instance,
+ * if the construction of FooImpl needed a BarService, then it could look
+ * something like this:
+ *
+ * ```
+ * const barRef = resolve(this, barToken);
+ * provide(this, fooToken, () => new FooImpl(barRef()));
+ * ```
+ *
+ * Lifetime guarantees
+ * ---
+ * A resolved dependency is valid for the duration of its component's connected
+ * lifetime.
+ *
+ * Internally, this is guaranteed by the following:
+ *
+ * - Dependency injection relies on using dom-events which work synchronously.
+ * - Dependency injection leverages ReactiveControllers whose lifetime
+ * mirror that of the component
+ * - Parent components' connected lifetime is guaranteed to include the
+ * connected lifetime of child components.
+ * - Dependency provider factories are only called during the lifetime of the
+ * component that provides the value.
+ *
+ * Best practices
+ * ===
+ * - Provide dependencies in or before connectedCallback
+ * - Verify that isConnected is true when accessing a dependency after an
+ * await.
+ *
+ * Type Safety
+ * ---
+ *
+ * Dependency injection is guaranteed npmtype-safe by construction due to the
+ * typing of the token used to tie together dependency providers and dependency
+ * consumers.
+ *
+ * Two tokens can never be equal because of how they are created. And both the
+ * consumer and provider logic of dependencies relies on the type of dependency
+ * token.
+ */
+
+/**
+ * A dependency-token is a unique key. It's typed by the type of the value the
+ * dependency needs.
+ */
+export type DependencyToken<ValueType> = symbol & {__type__: ValueType};
+
+/**
+ * Defines a unique dependency token for a given type. The string provided
+ * is purely for debugging and does not need to be unique.
+ *
+ * Example usage:
+ * const token = define<FooService>('foo-service');
+ */
+export function define<ValueType>(name: string) {
+ return Symbol(name) as unknown as DependencyToken<ValueType>;
+}
+
+/**
+ * A provider for a value.
+ */
+export type Provider<T> = () => T;
+
+/**
+ * A producer of a dependency expresses this as a need that results in a promise
+ * for the given dependency.
+ */
+export function provide<T>(
+ host: ReactiveControllerHost & HTMLElement,
+ dependency: DependencyToken<T>,
+ provider: Provider<T>
+) {
+ host.addController(new DependencyProvider<T>(host, dependency, provider));
+}
+
+/**
+ * A consumer of a service will resolve a given dependency token. The resolved
+ * dependency is returned as a simple function that can be called to access
+ * the injected value.
+ */
+export function resolve<T>(
+ host: ReactiveControllerHost & HTMLElement,
+ dependency: DependencyToken<T>
+): Provider<T> {
+ const controller = new DependencySubscriber(host, dependency);
+ host.addController(controller);
+ return () => controller.get();
+}
+
+/**
+ * Because Polymer doesn't (yet) depend on ReactiveControllerHost, this adds a
+ * work-around base-class to make this work for Polymer.
+ */
+export class DIPolymerElement
+ extends PolymerElement
+ implements ReactiveControllerHost
+{
+ private readonly ___controllers: ReactiveController[] = [];
+
+ override connectedCallback() {
+ for (const c of this.___controllers) {
+ c.hostConnected?.();
+ }
+ super.connectedCallback();
+ }
+
+ override disconnectedCallback() {
+ super.disconnectedCallback();
+ for (const c of this.___controllers) {
+ c.hostDisconnected?.();
+ }
+ }
+
+ addController(controller: ReactiveController) {
+ this.___controllers.push(controller);
+
+ if (this.isConnected) controller.hostConnected?.();
+ }
+
+ removeController(controller: ReactiveController) {
+ const idx = this.___controllers.indexOf(controller);
+ if (idx < 0) return;
+ this.___controllers?.splice(idx, 1);
+ }
+
+ requestUpdate() {}
+
+ get updateComplete(): Promise<boolean> {
+ return Promise.resolve(true);
+ }
+}
+
+/**
+ * A callback for a value.
+ */
+type Callback<T> = (value: T) => void;
+
+/**
+ * A Dependency Request gets sent by an element to ask for a dependency.
+ */
+export interface DependencyRequest<T> {
+ readonly dependency: DependencyToken<T>;
+ readonly callback: Callback<T>;
+}
+
+declare global {
+ interface HTMLElementEventMap {
+ /**
+ * An 'request-dependency' can be emitted by any element which desires a
+ * dependency to be injected by an external provider.
+ */
+ 'request-dependency': DependencyRequestEvent<unknown>;
+ }
+ interface DocumentEventMap {
+ /**
+ * An 'request-dependency' can be emitted by any element which desires a
+ * dependency to be injected by an external provider.
+ */
+ 'request-dependency': DependencyRequestEvent<unknown>;
+ }
+}
+
+/**
+ * Dependency Consumers fire DependencyRequests in the form of
+ * DependencyRequestEvent
+ */
+export class DependencyRequestEvent<T>
+ extends Event
+ implements DependencyRequest<T>
+{
+ public constructor(
+ public readonly dependency: DependencyToken<T>,
+ public readonly callback: Callback<T>
+ ) {
+ super('request-dependency', {bubbles: true, composed: true});
+ }
+}
+
+/**
+ * A resolved dependency is valid within the econnectd lifetime of a component,
+ * namely between connectedCallback and disconnectedCallback.
+ */
+interface ResolvedDependency<T> {
+ get(): T;
+}
+
+export class DependencyError<T> extends Error {
+ constructor(public readonly dependency: DependencyToken<T>, message: string) {
+ super(message);
+ }
+}
+
+class DependencySubscriber<T>
+ implements ReactiveController, ResolvedDependency<T>
+{
+ private value?: T;
+
+ private resolved = false;
+
+ constructor(
+ private readonly host: ReactiveControllerHost & HTMLElement,
+ private readonly dependency: DependencyToken<T>
+ ) {}
+
+ get() {
+ this.checkResolved();
+ return this.value!;
+ }
+
+ hostConnected() {
+ this.host.dispatchEvent(
+ new DependencyRequestEvent(this.dependency, (value: T) => {
+ this.resolved = true;
+ this.value = value;
+ })
+ );
+ 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);
+ }
+
+ hostDisconnected() {
+ this.value = undefined;
+ this.resolved = false;
+ }
+}
+
+class DependencyProvider<T> implements ReactiveController {
+ private value?: T;
+
+ constructor(
+ private readonly host: ReactiveControllerHost & HTMLElement,
+ private readonly dependency: DependencyToken<T>,
+ private readonly provider: Provider<T>
+ ) {}
+
+ 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!);
+ };
+}
diff --git a/polygerrit-ui/app/models/dependency_test.ts b/polygerrit-ui/app/models/dependency_test.ts
new file mode 100644
index 0000000..fa7cc29
--- /dev/null
+++ b/polygerrit-ui/app/models/dependency_test.ts
@@ -0,0 +1,199 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {define, provide, resolve, DIPolymerElement} from './dependency';
+import {html, LitElement} from 'lit';
+import {customElement as polyCustomElement} from '@polymer/decorators';
+import {html as polyHtml} from '@polymer/polymer/lib/utils/html-tag';
+import {customElement, property, query} from 'lit/decorators';
+import '../test/common-test-setup-karma.js';
+
+interface FooService {
+ value: string;
+}
+const fooToken = define<FooService>('foo');
+
+interface BarService {
+ value: string;
+}
+
+const barToken = define<BarService>('bar');
+
+class FooImpl implements FooService {
+ constructor(public readonly value: string) {}
+}
+
+class BarImpl implements BarService {
+ constructor(private readonly foo: FooService) {}
+
+ get value() {
+ return this.foo.value;
+ }
+}
+
+@customElement('lit-foo-provider')
+export class LitFooProviderElement extends LitElement {
+ @query('bar-provider')
+ bar?: BarProviderElement;
+
+ @property({type: Boolean})
+ public showBarProvider = true;
+
+ constructor() {
+ super();
+ provide(this, fooToken, () => new FooImpl('foo'));
+ }
+
+ override render() {
+ if (this.showBarProvider) {
+ return html`<bar-provider></bar-provider>`;
+ } else {
+ return undefined;
+ }
+ }
+}
+
+@polyCustomElement('polymer-foo-provider')
+export class PolymerFooProviderElement extends DIPolymerElement {
+ bar() {
+ return this.$.bar as BarProviderElement;
+ }
+
+ override connectedCallback() {
+ provide(this, fooToken, () => new FooImpl('foo'));
+ super.connectedCallback();
+ }
+
+ static get template() {
+ return polyHtml`<bar-provider id="bar"></bar-provider>`;
+ }
+}
+
+@customElement('bar-provider')
+export class BarProviderElement extends LitElement {
+ @query('leaf-lit-element')
+ litChild?: LeafLitElement;
+
+ @query('leaf-polymer-element')
+ polymerChild?: LeafPolymerElement;
+
+ @property({type: Boolean})
+ public showLit = true;
+
+ override connectedCallback() {
+ super.connectedCallback();
+ provide(this, barToken, () => this.create());
+ }
+
+ private create() {
+ const fooRef = resolve(this, fooToken);
+ assert.isDefined(fooRef());
+ return new BarImpl(fooRef());
+ }
+
+ override render() {
+ if (this.showLit) {
+ return html`<leaf-lit-element></leaf-lit-element>`;
+ } else {
+ return html`<leaf-polymer-element></leaf-polymer-element>`;
+ }
+ }
+}
+
+@customElement('leaf-lit-element')
+export class LeafLitElement extends LitElement {
+ readonly barRef = resolve(this, barToken);
+
+ override connectedCallback() {
+ super.connectedCallback();
+ assert.isDefined(this.barRef());
+ }
+
+ override render() {
+ return html`${this.barRef().value}`;
+ }
+}
+
+@polyCustomElement('leaf-polymer-element')
+export class LeafPolymerElement extends DIPolymerElement {
+ readonly barRef = resolve(this, barToken);
+
+ override connectedCallback() {
+ super.connectedCallback();
+ assert.isDefined(this.barRef());
+ }
+
+ static get template() {
+ return polyHtml`Hello`;
+ }
+}
+
+suite('Dependency', () => {
+ test('It instantiates', async () => {
+ const fixture = fixtureFromElement('lit-foo-provider');
+ const element = fixture.instantiate();
+ await element.updateComplete;
+ assert.isDefined(element.bar?.litChild?.barRef());
+ });
+
+ test('It instantiates in polymer', async () => {
+ const fixture = fixtureFromElement('polymer-foo-provider');
+ const element = fixture.instantiate();
+ await element.bar().updateComplete;
+ assert.isDefined(element.bar().litChild?.barRef());
+ });
+
+ test('It works by connecting and reconnecting', async () => {
+ const fixture = fixtureFromElement('lit-foo-provider');
+ const element = fixture.instantiate();
+ await element.updateComplete;
+ assert.isDefined(element.bar?.litChild?.barRef());
+
+ element.showBarProvider = false;
+ await element.updateComplete;
+ assert.isNull(element.bar);
+
+ element.showBarProvider = true;
+ await element.updateComplete;
+ assert.isDefined(element.bar?.litChild?.barRef());
+ });
+
+ test('It works by connecting and reconnecting Polymer', async () => {
+ const fixture = fixtureFromElement('lit-foo-provider');
+ const element = fixture.instantiate();
+ await element.updateComplete;
+
+ const beta = element.bar;
+ assert.isDefined(beta);
+ assert.isNotNull(beta);
+ assert.isDefined(element.bar?.litChild?.barRef());
+
+ beta!.showLit = false;
+ await element.updateComplete;
+ assert.isDefined(element.bar?.polymerChild?.barRef());
+ });
+});
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'lit-foo-provider': LitFooProviderElement;
+ 'polymer-foo-provider': PolymerFooProviderElement;
+ 'bar-provider': BarProviderElement;
+ 'leaf-lit-element': LeafLitElement;
+ 'leaf-polymer-element': LeafPolymerElement;
+ }
+}
diff --git a/polygerrit-ui/app/models/model.ts b/polygerrit-ui/app/models/model.ts
new file mode 100644
index 0000000..4a7f5ac
--- /dev/null
+++ b/polygerrit-ui/app/models/model.ts
@@ -0,0 +1,42 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {BehaviorSubject, Observable} from 'rxjs';
+
+/**
+ * A Model stores a value <T> and controls changes to that value via `subject$`
+ * while allowing others to subscribe to value updates via the `state$`
+ * Observable.
+ *
+ * Typically a given Model subclass will provide:
+ * 1. an initial value
+ * 2. "reducers": functions for users to request changes to the value
+ * 3. "selectors": convenient sub-Observables that only contain updates for a
+ * nested property from the value
+ *
+ * Any new subscriber will immediately receive the current value.
+ */
+export abstract class Model<T> {
+ protected subject$: BehaviorSubject<T>;
+
+ public state$: Observable<T>;
+
+ constructor(initialState: T) {
+ this.subject$ = new BehaviorSubject(initialState);
+ this.state$ = this.subject$.asObservable();
+ }
+}
diff --git a/polygerrit-ui/app/models/user/user-model.ts b/polygerrit-ui/app/models/user/user-model.ts
new file mode 100644
index 0000000..5e9ed3f
--- /dev/null
+++ b/polygerrit-ui/app/models/user/user-model.ts
@@ -0,0 +1,183 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {from, of, Observable, Subscription} from 'rxjs';
+import {switchMap} from 'rxjs/operators';
+import {
+ DiffPreferencesInfo as DiffPreferencesInfoAPI,
+ DiffViewMode,
+} from '../../api/diff';
+import {
+ AccountCapabilityInfo,
+ AccountDetailInfo,
+ PreferencesInfo,
+} from '../../types/common';
+import {
+ createDefaultPreferences,
+ createDefaultDiffPrefs,
+} 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 {Model} from '../model';
+
+export interface UserState {
+ /**
+ * Keeps being defined even when credentials have expired.
+ */
+ account?: AccountDetailInfo;
+ preferences: PreferencesInfo;
+ diffPreferences: DiffPreferencesInfo;
+ capabilities?: AccountCapabilityInfo;
+}
+
+export class UserModel extends Model<UserState> implements Finalizable {
+ readonly account$: Observable<AccountDetailInfo | undefined> = select(
+ this.state$,
+ userState => userState.account
+ );
+
+ /** Note that this may still be true, even if credentials have expired. */
+ readonly loggedIn$: Observable<boolean> = select(
+ this.account$,
+ account => !!account
+ );
+
+ readonly capabilities$: Observable<AccountCapabilityInfo | undefined> =
+ select(this.state$, userState => userState.capabilities);
+
+ readonly isAdmin$: Observable<boolean> = select(
+ this.capabilities$,
+ capabilities => capabilities?.administrateServer ?? false
+ );
+
+ readonly preferences$: Observable<PreferencesInfo> = select(
+ this.state$,
+ userState => userState.preferences
+ );
+
+ readonly diffPreferences$: Observable<DiffPreferencesInfo> = select(
+ this.state$,
+ userState => userState.diffPreferences
+ );
+
+ readonly preferenceDiffViewMode$: Observable<DiffViewMode> = select(
+ this.preferences$,
+ preference => preference.diff_view ?? DiffViewMode.SIDE_BY_SIDE
+ );
+
+ private subscriptions: Subscription[] = [];
+
+ constructor(readonly restApiService: RestApiService) {
+ super({
+ preferences: createDefaultPreferences(),
+ diffPreferences: createDefaultDiffPrefs(),
+ });
+ this.subscriptions = [
+ from(this.restApiService.getAccount()).subscribe(
+ (account?: AccountDetailInfo) => {
+ this.setAccount(account);
+ }
+ ),
+ this.account$
+ .pipe(
+ switchMap(account => {
+ if (!account) return of(createDefaultPreferences());
+ return from(this.restApiService.getPreferences());
+ })
+ )
+ .subscribe((preferences?: PreferencesInfo) => {
+ this.setPreferences(preferences ?? createDefaultPreferences());
+ }),
+ this.account$
+ .pipe(
+ switchMap(account => {
+ if (!account) return of(createDefaultDiffPrefs());
+ return from(this.restApiService.getDiffPreferences());
+ })
+ )
+ .subscribe((diffPrefs?: DiffPreferencesInfoAPI) => {
+ this.setDiffPreferences(diffPrefs ?? createDefaultDiffPrefs());
+ }),
+ this.account$
+ .pipe(
+ switchMap(account => {
+ if (!account) return of(undefined);
+ return from(this.restApiService.getAccountCapabilities());
+ })
+ )
+ .subscribe((capabilities?: AccountCapabilityInfo) => {
+ this.setCapabilities(capabilities);
+ }),
+ ];
+ }
+
+ finalize() {
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions = [];
+ }
+
+ updatePreferences(prefs: Partial<PreferencesInfo>) {
+ this.restApiService
+ .savePreferences(prefs)
+ .then((newPrefs: PreferencesInfo | undefined) => {
+ if (!newPrefs) return;
+ this.setPreferences(newPrefs);
+ });
+ }
+
+ updateDiffPreference(diffPrefs: DiffPreferencesInfo) {
+ return this.restApiService
+ .saveDiffPreferences(diffPrefs)
+ .then((response: Response) => {
+ this.restApiService.getResponseObject(response).then(obj => {
+ const newPrefs = obj as unknown as DiffPreferencesInfo;
+ if (!newPrefs) return;
+ this.setDiffPreferences(newPrefs);
+ });
+ });
+ }
+
+ getDiffPreferences() {
+ return this.restApiService.getDiffPreferences().then(prefs => {
+ if (!prefs) return;
+ this.setDiffPreferences(prefs);
+ });
+ }
+
+ setPreferences(preferences: PreferencesInfo) {
+ const current = this.subject$.getValue();
+ this.subject$.next({...current, preferences});
+ }
+
+ setDiffPreferences(diffPreferences: DiffPreferencesInfo) {
+ const current = this.subject$.getValue();
+ this.subject$.next({...current, diffPreferences});
+ }
+
+ setCapabilities(capabilities?: AccountCapabilityInfo) {
+ const current = this.subject$.getValue();
+ this.subject$.next({...current, capabilities});
+ }
+
+ private setAccount(account?: AccountDetailInfo) {
+ const current = this.subject$.getValue();
+ this.subject$.next({...current, account});
+ }
+}
diff --git a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.js b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.js
index 989bafb..223c2ab 100644
--- a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.js
+++ b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.js
@@ -17,7 +17,7 @@
import '../../test/common-test-setup-karma.js';
import {GrEmailSuggestionsProvider} from './gr-email-suggestions-provider.js';
-import {appContext} from '../../services/app-context.js';
+import {getAppContext} from '../../services/app-context.js';
import {stubRestApi} from '../../test/test-utils.js';
suite('GrEmailSuggestionsProvider tests', () => {
@@ -32,7 +32,7 @@
};
setup(() => {
- provider = new GrEmailSuggestionsProvider(appContext.restApiService);
+ provider = new GrEmailSuggestionsProvider(getAppContext().restApiService);
});
test('getSuggestions', async () => {
diff --git a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.js b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.js
index 67f9433..e643166 100644
--- a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.js
+++ b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.js
@@ -17,7 +17,7 @@
import '../../test/common-test-setup-karma.js';
import {GrGroupSuggestionsProvider} from './gr-group-suggestions-provider.js';
-import {appContext} from '../../services/app-context.js';
+import {getAppContext} from '../../services/app-context.js';
import {stubRestApi} from '../../test/test-utils.js';
suite('GrGroupSuggestionsProvider tests', () => {
@@ -33,7 +33,7 @@
};
setup(() => {
- provider = new GrGroupSuggestionsProvider(appContext.restApiService);
+ provider = new GrGroupSuggestionsProvider(getAppContext().restApiService);
});
test('getSuggestions', async () => {
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.js b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.js
index 762d36c..1916822 100644
--- a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.js
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.js
@@ -17,7 +17,7 @@
import '../../test/common-test-setup-karma.js';
import {GrReviewerSuggestionsProvider, SUGGESTIONS_PROVIDERS_USERS_TYPES} from './gr-reviewer-suggestions-provider.js';
-import {appContext} from '../../services/app-context.js';
+import {getAppContext} from '../../services/app-context.js';
import {stubRestApi} from '../../test/test-utils.js';
suite('GrReviewerSuggestionsProvider tests', () => {
@@ -84,7 +84,7 @@
suite('allowAnyUser set to false', () => {
setup(async () => {
provider = GrReviewerSuggestionsProvider.create(
- appContext.restApiService, change._number,
+ getAppContext().restApiService, change._number,
SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER);
await provider.init();
});
@@ -204,7 +204,7 @@
suite('allowAnyUser set to true', () => {
setup(async () => {
provider = GrReviewerSuggestionsProvider.create(
- appContext.restApiService, change._number,
+ getAppContext().restApiService, change._number,
SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY);
await provider.init();
});
diff --git a/polygerrit-ui/app/services/README.md b/polygerrit-ui/app/services/README.md
index b88532b..126db83 100644
--- a/polygerrit-ui/app/services/README.md
+++ b/polygerrit-ui/app/services/README.md
@@ -9,20 +9,17 @@
Regarding all stateful should be considered as services or not, it's still TBD. Will update as soon
as it's finalized.
-## How to access service
+## How to access a service
We use AppContext to access instance of service. It helps in mocking service in tests as well.
We prefer setting instance of service in constructor and then accessing it from variable. We also
-allow access straight from appContext especially in static methods.
+allow access straight from getAppContext() especially in static methods.
```
-import {appContext} from '../../../services/app-context.js';
+import {getAppContext()} from '../../../services/app-context.js';
class T {
- constructor() {
- super();
- this.flagsService = appContext.flagsService;
- }
+ private readonly flagsService = getAppContext().flagsService;
action1() {
if (this.flagsService.isEnabled('test)) {
@@ -45,10 +42,10 @@
'flags' is a service to provide easy access to all enabled experiments.
```
-import {appContext} from '../../../services/app-context.js';
+import {getAppContext} from '../../../services/app-context.js';
// check if an experiment is enabled or not
-if (appContext.flagsService.isEnabled('test')) {
+if (getAppContext().flagsService.isEnabled('test')) {
// do something
}
-```
\ No newline at end of file
+```
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index 3a6f7c5..81d64e02 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -14,75 +14,108 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {appContext, AppContext} from './app-context';
+import {AppContext} from './app-context';
+import {create, Finalizable, Registry} from './registry';
+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 {GrRestApiInterface} from '../elements/shared/gr-rest-api-interface/gr-rest-api-interface';
-import {ChangeService} from './change/change-service';
-import {ChecksService} from './checks/checks-service';
+import {GrRestApiServiceImpl} from '../elements/shared/gr-rest-api-interface/gr-rest-api-impl';
+import {ChangeModel} from './change/change-model';
+import {ChecksModel} from './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 {ConfigService} from './config/config-service';
-import {UserService} from './user/user-service';
-import {CommentsService} from './comments/comments-service';
+import {UserModel} from '../models/user/user-model';
+import {
+ CommentsModel,
+ commentsModelToken,
+} from '../models/comments/comments-model';
+import {RouterModel} from './router/router-model';
import {ShortcutsService} from './shortcuts/shortcuts-service';
-
-type ServiceName = keyof AppContext;
-type ServiceCreator<T> = () => T;
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-const initializedServices: Map<ServiceName, any> = new Map<ServiceName, any>();
-
-function getService<K extends ServiceName>(
- serviceName: K,
- serviceCreator: ServiceCreator<AppContext[K]>
-): AppContext[K] {
- if (!initializedServices.has(serviceName)) {
- initializedServices.set(serviceName, serviceCreator());
- }
- return initializedServices.get(serviceName);
-}
+import {assertIsDefined} from '../utils/common-util';
+import {ConfigModel, configModelToken} from '../models/config/config-model';
+import {BrowserModel, browserModelToken} from '../models/browser/browser-model';
/**
* The AppContext lazy initializator for all services
*/
-export function initAppContext() {
- function populateAppContext(
- serviceCreators: {[P in ServiceName]: ServiceCreator<AppContext[P]>}
- ) {
- const registeredServices = Object.keys(serviceCreators).reduce(
- (registeredServices, key) => {
- const serviceName = key as ServiceName;
- const serviceCreator = serviceCreators[serviceName];
- registeredServices[serviceName] = {
- configurable: true, // Tests can mock properties
- get() {
- return getService(serviceName, serviceCreator);
- },
- };
- return registeredServices;
- },
- {} as PropertyDescriptorMap
- );
- Object.defineProperties(appContext, registeredServices);
- }
+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!);
+ },
+ restApiService: (ctx: Partial<AppContext>) => {
+ assertIsDefined(ctx.authService, 'authService');
+ assertIsDefined(ctx.flagsService, 'flagsService)');
+ return new GrRestApiServiceImpl(ctx.authService!, ctx.flagsService!);
+ },
+ changeModel: (ctx: Partial<AppContext>) => {
+ const routerModel = ctx.routerModel;
+ const restApiService = ctx.restApiService;
+ const userModel = ctx.userModel;
+ assertIsDefined(routerModel, 'routerModel');
+ assertIsDefined(restApiService, 'restApiService');
+ assertIsDefined(userModel, 'userModel');
+ return new ChangeModel(routerModel, restApiService, userModel);
+ },
+ checksModel: (ctx: Partial<AppContext>) => {
+ const routerModel = ctx.routerModel;
+ const changeModel = ctx.changeModel;
+ const reportingService = ctx.reportingService;
+ assertIsDefined(routerModel, 'routerModel');
+ assertIsDefined(changeModel, 'changeModel');
+ assertIsDefined(reportingService, 'reportingService');
+ return new ChecksModel(routerModel, changeModel, reportingService);
+ },
+ 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!);
+ },
+ shortcutsService: (ctx: Partial<AppContext>) => {
+ assertIsDefined(ctx.userModel, 'userModel');
+ assertIsDefined(ctx.reportingService, 'reportingService');
+ return new ShortcutsService(ctx.userModel, ctx.reportingService!);
+ },
+ };
+ return create<AppContext>(appRegistry);
+}
- populateAppContext({
- flagsService: () => new FlagsServiceImplementation(),
- reportingService: () => new GrReporting(appContext.flagsService),
- eventEmitter: () => new EventEmitter(),
- authService: () => new Auth(appContext.eventEmitter),
- restApiService: () =>
- new GrRestApiInterface(appContext.authService, appContext.flagsService),
- changeService: () => new ChangeService(),
- commentsService: () => new CommentsService(appContext.restApiService),
- checksService: () => new ChecksService(appContext.reportingService),
- jsApiService: () => new GrJsApiInterface(),
- storageService: () => new GrStorageService(),
- configService: () => new ConfigService(),
- userService: () => new UserService(appContext.restApiService),
- shortcutsService: () => new ShortcutsService(appContext.reportingService),
- });
+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 commentsModel = new CommentsModel(
+ appContext.routerModel,
+ appContext.changeModel,
+ appContext.restApiService,
+ appContext.reportingService
+ );
+ dependencies.set(commentsModelToken, commentsModel);
+
+ const configModel = new ConfigModel(
+ appContext.changeModel,
+ appContext.restApiService
+ );
+ dependencies.set(configModelToken, configModel);
+
+ return dependencies;
}
diff --git a/polygerrit-ui/app/services/app-context-init_test.js b/polygerrit-ui/app/services/app-context-init_test.ts
similarity index 68%
rename from polygerrit-ui/app/services/app-context-init_test.js
rename to polygerrit-ui/app/services/app-context-init_test.ts
index 9d22ec2..efe6aac 100644
--- a/polygerrit-ui/app/services/app-context-init_test.js
+++ b/polygerrit-ui/app/services/app-context-init_test.ts
@@ -16,20 +16,27 @@
*/
import '../test/common-test-setup-karma.js';
-import {appContext} from './app-context.js';
-import {initAppContext} from './app-context-init.js';
+import {AppContext} from './app-context.js';
+import {createAppContext} from './app-context-init.js';
+import {Finalizable} from './registry';
+
suite('app context initializer tests', () => {
+ let appContext: AppContext & Finalizable;
setup(() => {
- initAppContext();
+ appContext = createAppContext();
+ });
+
+ teardown(() => {
+ appContext.finalize();
});
test('all services initialized and are singletons', () => {
Object.keys(appContext).forEach(serviceName => {
- const service = appContext[serviceName];
+ const service = appContext[serviceName as keyof AppContext];
+ assert.isDefined(service);
assert.isNotNull(service);
- const service2 = appContext[serviceName];
+ const service2 = appContext[serviceName as keyof AppContext];
assert.strictEqual(service, service2);
});
});
});
-
diff --git a/polygerrit-ui/app/services/app-context.ts b/polygerrit-ui/app/services/app-context.ts
index e5828d6..6d6afc9 100644
--- a/polygerrit-ui/app/services/app-context.ts
+++ b/polygerrit-ui/app/services/app-context.ts
@@ -14,43 +14,52 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+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 {ChangeService} from './change/change-service';
-import {ChecksService} from './checks/checks-service';
+import {ChangeModel} from './change/change-model';
+import {ChecksModel} from './checks/checks-model';
import {JsApiService} from '../elements/shared/gr-js-api-interface/gr-js-api-types';
import {StorageService} from './storage/gr-storage';
-import {ConfigService} from './config/config-service';
-import {UserService} from './user/user-service';
-import {CommentsService} from './comments/comments-service';
+import {UserModel} from '../models/user/user-model';
+import {RouterModel} from './router/router-model';
import {ShortcutsService} from './shortcuts/shortcuts-service';
export interface AppContext {
+ routerModel: RouterModel;
flagsService: FlagsService;
reportingService: ReportingService;
eventEmitter: EventEmitterService;
authService: AuthService;
restApiService: RestApiService;
- changeService: ChangeService;
- commentsService: CommentsService;
- checksService: ChecksService;
+ changeModel: ChangeModel;
+ checksModel: ChecksModel;
jsApiService: JsApiService;
storageService: StorageService;
- configService: ConfigService;
- userService: UserService;
+ userModel: UserModel;
shortcutsService: ShortcutsService;
}
/**
- * The AppContext holds immortal singleton instances of services. It's a
- * convenient way to provide singletons that can be swapped out for testing.
+ * The AppContext holds instances of services. It's a convenient way to provide
+ * singletons that can be swapped out for testing.
*
* AppContext is initialized in ./app-context-init.js
*
* It is guaranteed that all fields in appContext are always initialized
* (except for shared gr-diff)
*/
-export const appContext: AppContext = {} as AppContext;
+let appContext: (AppContext & Finalizable) | undefined = undefined;
+
+export function injectAppContext(ctx: AppContext & Finalizable) {
+ appContext?.finalize();
+ appContext = ctx;
+}
+
+export function getAppContext() {
+ if (!appContext) throw new Error('App context has not been injected');
+ return appContext;
+}
diff --git a/polygerrit-ui/app/services/change/change-model.ts b/polygerrit-ui/app/services/change/change-model.ts
index 962ef4d..6bf7c103 100644
--- a/polygerrit-ui/app/services/change/change-model.ts
+++ b/polygerrit-ui/app/services/change/change-model.ts
@@ -15,106 +15,388 @@
* limitations under the License.
*/
-import {PatchSetNum} from '../../types/common';
-import {BehaviorSubject, combineLatest, Observable} from 'rxjs';
+import {
+ EditInfo,
+ EditPatchSetNum,
+ NumericChangeId,
+ PatchSetNum,
+} from '../../types/common';
+import {
+ combineLatest,
+ from,
+ fromEvent,
+ Observable,
+ Subscription,
+ forkJoin,
+ of,
+} from 'rxjs';
import {
map,
filter,
withLatestFrom,
distinctUntilChanged,
+ startWith,
+ switchMap,
} from 'rxjs/operators';
-import {routerPatchNum$, routerState$} from '../router/router-model';
+import {RouterModel} from '../router/router-model';
import {
computeAllPatchSets,
computeLatestPatchNum,
} from '../../utils/patch-set-util';
import {ParsedChangeInfo} from '../../types/types';
+import {fireAlert} from '../../utils/event-util';
-interface ChangeState {
+import {ChangeInfo} from '../../types/common';
+import {RestApiService} from '../gr-rest-api/gr-rest-api';
+import {Finalizable} from '../registry';
+import {select} from '../../utils/observable-util';
+import {assertIsDefined} from '../../utils/common-util';
+import {Model} from '../../models/model';
+import {UserModel} from '../../models/user/user-model';
+
+export enum LoadingStatus {
+ NOT_LOADED = 'NOT_LOADED',
+ LOADING = 'LOADING',
+ RELOADING = 'RELOADING',
+ LOADED = 'LOADED',
+}
+
+const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
+
+export interface ChangeState {
+ /**
+ * If `change` is undefined, this must be either NOT_LOADED or LOADING.
+ * If `change` is defined, this must be either LOADED or RELOADING.
+ */
+ loadingStatus: LoadingStatus;
change?: ParsedChangeInfo;
+ /**
+ * The name of the file user is viewing in the diff view mode. File path is
+ * specified in the url or derived from the commentId.
+ * Does not apply to change-view or edit-view.
+ */
+ diffPath?: string;
+ /**
+ * The list of reviewed files, kept in the model because we want changes made
+ * in one view to reflect on other views without re-rendering the other views.
+ * Undefined means it's still loading and empty set means no files reviewed.
+ */
+ reviewedFiles?: string[];
+}
+
+/**
+ * Updates the change object with information from the saved `edit` patchset.
+ */
+// visible for testing
+export function updateChangeWithEdit(
+ change?: ParsedChangeInfo,
+ edit?: EditInfo,
+ routerPatchNum?: PatchSetNum
+): ParsedChangeInfo | undefined {
+ if (!change || !edit) return change;
+ assertIsDefined(edit.commit.commit, 'edit.commit.commit');
+ if (!change.revisions) change.revisions = {};
+ change.revisions[edit.commit.commit] = {
+ _number: EditPatchSetNum,
+ basePatchNum: edit.base_patch_set_number,
+ commit: edit.commit,
+ fetch: edit.fetch,
+ };
+ // If the change was loaded without a specific patchset, then this normally
+ // means that the *latest* patchset should be loaded. But if there is an
+ // active edit, then automatically switch to that edit as the current
+ // patchset.
+ // TODO: This goes together with `_patchRange.patchNum' being set to `edit`,
+ // which is still done in change-view. `_patchRange.patchNum` should
+ // eventually also be model managed, so we can reconcile these two code
+ // snippets into one location.
+ if (routerPatchNum === undefined) {
+ change.current_revision = edit.commit.commit;
+ }
+ return change;
}
// TODO: Figure out how to best enforce immutability of all states. Use Immer?
// Use DeepReadOnly?
-const initialState: ChangeState = {};
+const initialState: ChangeState = {
+ loadingStatus: LoadingStatus.NOT_LOADED,
+};
-const privateState$ = new BehaviorSubject(initialState);
+export class ChangeModel extends Model<ChangeState> implements Finalizable {
+ private change?: ParsedChangeInfo;
-// Re-exporting as Observable so that you can only subscribe, but not emit.
-export const changeState$: Observable<ChangeState> = privateState$;
+ private currentPatchNum?: PatchSetNum;
-// Must only be used by the change service or whatever is in control of this
-// model.
-export function updateState(change?: ParsedChangeInfo) {
- const current = privateState$.getValue();
- // We want to make it easy for subscribers to react to change changes, so we
- // are explicitly emitting an additional `undefined` when the change number
- // changes. So if you are subscribed to the latestPatchsetNumber for example,
- // then you can rely on emissions even if the old and the new change have the
- // same latestPatchsetNumber.
- if (change !== undefined && current.change !== undefined) {
- if (change._number !== current.change._number) {
- privateState$.next({...current, change: undefined});
- }
- }
- privateState$.next({...current, change});
-}
-
-/**
- * If you depend on both, router and change state, then you want to filter out
- * inconsistent state, e.g. router changeNum already updated, change not yet
- * reset to undefined.
- */
-export const changeAndRouterConsistent$ = combineLatest([
- routerState$,
- changeState$,
-]).pipe(
- filter(([routerState, changeState]) => {
- const changeNum = changeState.change?._number;
- const routerChangeNum = routerState.changeNum;
- return changeNum === undefined || changeNum === routerChangeNum;
- }),
- distinctUntilChanged()
-);
-
-export const change$ = changeState$.pipe(
- map(changeState => changeState.change),
- distinctUntilChanged()
-);
-
-export const changeNum$ = change$.pipe(
- map(change => change?._number),
- distinctUntilChanged()
-);
-
-export const repo$ = change$.pipe(
- map(change => change?.project),
- distinctUntilChanged()
-);
-
-export const labels$ = change$.pipe(
- map(change => change?.labels),
- distinctUntilChanged()
-);
-
-export const latestPatchNum$ = change$.pipe(
- map(change => computeLatestPatchNum(computeAllPatchSets(change))),
- distinctUntilChanged()
-);
-
-/**
- * Emits the current patchset number. If the route does not define the current
- * patchset num, then this selector waits for the change to be defined and
- * returns the number of the latest patchset.
- *
- * Note that this selector can emit a patchNum without the change being
- * available!
- */
-export const currentPatchNum$: Observable<PatchSetNum | undefined> =
- changeAndRouterConsistent$.pipe(
- withLatestFrom(routerPatchNum$, latestPatchNum$),
- map(
- ([_, routerPatchNum, latestPatchNum]) => routerPatchNum || latestPatchNum
- ),
- distinctUntilChanged()
+ public readonly change$ = select(
+ this.state$,
+ changeState => changeState.change
);
+
+ public readonly changeLoadingStatus$ = select(
+ this.state$,
+ changeState => changeState.loadingStatus
+ );
+
+ public readonly diffPath$ = select(
+ this.state$,
+ changeState => changeState?.diffPath
+ );
+
+ public readonly reviewedFiles$ = select(
+ this.state$,
+ changeState => changeState?.reviewedFiles
+ );
+
+ public readonly changeNum$ = select(this.change$, change => change?._number);
+
+ public readonly repo$ = select(this.change$, change => change?.project);
+
+ public readonly labels$ = select(this.change$, change => change?.labels);
+
+ public readonly latestPatchNum$ = select(this.change$, change =>
+ computeLatestPatchNum(computeAllPatchSets(change))
+ );
+
+ /**
+ * Emits the current patchset number. If the route does not define the current
+ * patchset num, then this selector waits for the change to be defined and
+ * returns the number of the latest patchset.
+ *
+ * Note that this selector can emit a patchNum without the change being
+ * available!
+ */
+ public readonly currentPatchNum$: Observable<PatchSetNum | undefined> =
+ /**
+ * If you depend on both, router and change state, then you want to filter
+ * out inconsistent state, e.g. router changeNum already updated, change not
+ * yet reset to undefined.
+ */
+ combineLatest([this.routerModel.state$, this.state$])
+ .pipe(
+ filter(([routerState, changeState]) => {
+ const changeNum = changeState.change?._number;
+ const routerChangeNum = routerState.changeNum;
+ return changeNum === undefined || changeNum === routerChangeNum;
+ }),
+ distinctUntilChanged()
+ )
+ .pipe(
+ withLatestFrom(this.routerModel.routerPatchNum$, this.latestPatchNum$),
+ map(([_, routerPatchN, latestPatchN]) => routerPatchN || latestPatchN),
+ distinctUntilChanged()
+ );
+
+ private subscriptions: Subscription[] = [];
+
+ // 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));
+
+ constructor(
+ readonly routerModel: RouterModel,
+ readonly restApiService: RestApiService,
+ readonly userModel: UserModel
+ ) {
+ super(initialState);
+ this.subscriptions = [
+ combineLatest([this.routerModel.routerChangeNum$, this.reload$])
+ .pipe(
+ map(([changeNum, _]) => changeNum),
+ switchMap(changeNum => {
+ if (changeNum !== undefined) this.updateStateLoading(changeNum);
+ const change = from(this.restApiService.getChangeDetail(changeNum));
+ const edit = from(this.restApiService.getChangeEdit(changeNum));
+ return forkJoin([change, edit]);
+ }),
+ withLatestFrom(this.routerModel.routerPatchNum$),
+ map(([[change, edit], patchNum]) =>
+ updateChangeWithEdit(change, edit, patchNum)
+ )
+ )
+ .subscribe(change => {
+ // The change service is currently a singleton, so we have to be
+ // careful to avoid situations where the application state is
+ // partially set for the old change where the user is coming from,
+ // and partially for the new change where the user is navigating to.
+ // So setting the change explicitly to undefined when the user
+ // moves away from diff and change pages (changeNum === undefined)
+ // helps with that.
+ this.updateStateChange(change ?? undefined);
+ }),
+ this.change$.subscribe(change => (this.change = change)),
+ this.currentPatchNum$.subscribe(
+ currentPatchNum => (this.currentPatchNum = currentPatchNum)
+ ),
+ combineLatest([
+ this.currentPatchNum$,
+ this.changeNum$,
+ this.userModel.loggedIn$,
+ ])
+ .pipe(
+ switchMap(([currentPatchNum, changeNum, loggedIn]) => {
+ if (!changeNum || !currentPatchNum || !loggedIn) {
+ this.updateStateReviewedFiles([]);
+ return of(undefined);
+ }
+ return from(this.fetchReviewedFiles(currentPatchNum!, changeNum!));
+ })
+ )
+ .subscribe(),
+ ];
+ }
+
+ finalize() {
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions = [];
+ }
+
+ // Temporary workaround until path is derived in the model itself.
+ updatePath(diffPath?: string) {
+ const current = this.subject$.getValue();
+ this.setState({...current, diffPath});
+ }
+
+ updateStateReviewedFiles(reviewedFiles: string[]) {
+ const current = this.subject$.getValue();
+ this.setState({...current, reviewedFiles});
+ }
+
+ updateStateFileReviewed(file: string, reviewed: boolean) {
+ const current = this.subject$.getValue();
+ if (current.reviewedFiles === undefined) {
+ // Reviewed files haven't loaded yet.
+ // TODO(dhruvsri): disable updating status if reviewed files are not loaded.
+ fireAlert(
+ document,
+ 'Updating status failed. Reviewed files not loaded yet.'
+ );
+ return;
+ }
+ const reviewedFiles = [...current.reviewedFiles];
+
+ // File is already reviewed and is being marked reviewed
+ if (reviewedFiles.includes(file) && reviewed) return;
+ // File is not reviewed and is being marked not reviewed
+ if (!reviewedFiles.includes(file) && !reviewed) return;
+
+ if (reviewed) reviewedFiles.push(file);
+ else reviewedFiles.splice(reviewedFiles.indexOf(file), 1);
+ this.setState({...current, reviewedFiles});
+ }
+
+ fetchReviewedFiles(currentPatchNum: PatchSetNum, changeNum: NumericChangeId) {
+ return this.restApiService
+ .getReviewedFiles(changeNum, currentPatchNum)
+ .then(files => {
+ if (
+ changeNum !== this.change?._number ||
+ currentPatchNum !== this.currentPatchNum
+ )
+ return;
+ this.updateStateReviewedFiles(files ?? []);
+ });
+ }
+
+ setReviewedFilesStatus(
+ changeNum: NumericChangeId,
+ patchNum: PatchSetNum,
+ file: string,
+ reviewed: boolean
+ ) {
+ return this.restApiService
+ .saveFileReviewed(changeNum, patchNum, file, reviewed)
+ .then(() => {
+ if (
+ changeNum !== this.change?._number ||
+ patchNum !== this.currentPatchNum
+ )
+ return;
+ this.updateStateFileReviewed(file, reviewed);
+ })
+ .catch(() => {
+ fireAlert(document, ERR_REVIEW_STATUS);
+ });
+ }
+
+ /**
+ * Typically you would just subscribe to change$ yourself to get updates. But
+ * sometimes it is nice to also be able to get the current ChangeInfo on
+ * demand. So here it is for your convenience.
+ */
+ getChange() {
+ return this.subject$.getValue().change;
+ }
+
+ /**
+ * Check whether there is no newer patch than the latest patch that was
+ * available when this change was loaded.
+ *
+ * @return A promise that yields true if the latest patch
+ * has been loaded, and false if a newer patch has been uploaded in the
+ * meantime. The promise is rejected on network error.
+ */
+ fetchChangeUpdates(change: ChangeInfo | ParsedChangeInfo) {
+ const knownLatest = computeLatestPatchNum(computeAllPatchSets(change));
+ return this.restApiService.getChangeDetail(change._number).then(detail => {
+ if (!detail) {
+ const error = new Error('Change detail not found.');
+ return Promise.reject(error);
+ }
+ const actualLatest = computeLatestPatchNum(computeAllPatchSets(detail));
+ if (!actualLatest || !knownLatest) {
+ const error = new Error('Unable to check for latest patchset.');
+ return Promise.reject(error);
+ }
+ return {
+ isLatest: actualLatest <= knownLatest,
+ newStatus: change.status !== detail.status ? detail.status : null,
+ newMessages:
+ (change.messages || []).length < (detail.messages || []).length
+ ? detail.messages![detail.messages!.length - 1]
+ : undefined,
+ };
+ });
+ }
+
+ /**
+ * Called when change detail loading is initiated.
+ *
+ * If the change number matches the current change in the state, then
+ * this is a reload. If not, then we not just want to set the state to
+ * LOADING instead of RELOADING, but we also want to set the change to
+ * undefined right away. Otherwise components could see inconsistent state:
+ * a new change number, but an old change.
+ */
+ private updateStateLoading(changeNum: NumericChangeId) {
+ const current = this.subject$.getValue();
+ const reloading = current.change?._number === changeNum;
+ this.setState({
+ ...current,
+ change: reloading ? current.change : undefined,
+ loadingStatus: reloading
+ ? LoadingStatus.RELOADING
+ : LoadingStatus.LOADING,
+ });
+ }
+
+ // Private but used in tests.
+ updateStateChange(change?: ParsedChangeInfo) {
+ const current = this.subject$.getValue();
+ this.setState({
+ ...current,
+ change,
+ loadingStatus:
+ change === undefined ? LoadingStatus.NOT_LOADED : LoadingStatus.LOADED,
+ });
+ }
+
+ // Private but used in tests
+ setState(state: ChangeState) {
+ this.subject$.next(state);
+ }
+}
diff --git a/polygerrit-ui/app/services/change/change-model_test.ts b/polygerrit-ui/app/services/change/change-model_test.ts
new file mode 100644
index 0000000..37924c1
--- /dev/null
+++ b/polygerrit-ui/app/services/change/change-model_test.ts
@@ -0,0 +1,298 @@
+/**
+ * @license
+ * 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.
+ */
+
+import {Subject} from 'rxjs';
+import {takeUntil} from 'rxjs/operators';
+import {ChangeStatus} from '../../constants/constants';
+import '../../test/common-test-setup-karma';
+import {
+ createChange,
+ createChangeMessageInfo,
+ createEditInfo,
+ createParsedChange,
+ createRevision,
+} from '../../test/test-data-generators';
+import {mockPromise, stubRestApi, waitUntil} from '../../test/test-utils';
+import {
+ CommitId,
+ EditPatchSetNum,
+ NumericChangeId,
+ PatchSetNum,
+} from '../../types/common';
+import {ParsedChangeInfo} from '../../types/types';
+import {getAppContext} from '../app-context';
+import {GerritView} from '../router/router-model';
+import {ChangeState, LoadingStatus, updateChangeWithEdit} from './change-model';
+import {ChangeModel} from './change-model';
+
+suite('updateChangeWithEdit() tests', () => {
+ test('undefined change', async () => {
+ assert.isUndefined(updateChangeWithEdit());
+ });
+
+ test('undefined edit', async () => {
+ const change = createParsedChange();
+ assert.equal(updateChangeWithEdit(change), change);
+ });
+
+ test('set edit rev and current rev', async () => {
+ let change: ParsedChangeInfo | undefined = createParsedChange();
+ const edit = createEditInfo();
+ change = updateChangeWithEdit(change, edit);
+ const editRev = change?.revisions[`${edit.commit.commit}`];
+ assert.isDefined(editRev);
+ assert.equal(editRev?._number, EditPatchSetNum);
+ assert.equal(editRev?.basePatchNum, edit.base_patch_set_number);
+ assert.equal(change?.current_revision, edit.commit.commit);
+ });
+
+ test('do not set current rev when patchNum already set', async () => {
+ let change: ParsedChangeInfo | undefined = createParsedChange();
+ const edit = createEditInfo();
+ change = updateChangeWithEdit(change, edit, 1 as PatchSetNum);
+ const editRev = change?.revisions[`${edit.commit.commit}`];
+ assert.isDefined(editRev);
+ assert.equal(change?.current_revision, 'abc' as CommitId);
+ });
+});
+
+suite('change service tests', () => {
+ let changeModel: ChangeModel;
+ let knownChange: ParsedChangeInfo;
+ const testCompleted = new Subject<void>();
+ setup(() => {
+ changeModel = new ChangeModel(
+ getAppContext().routerModel,
+ getAppContext().restApiService,
+ getAppContext().userModel
+ );
+ knownChange = {
+ ...createChange(),
+ revisions: {
+ sha1: {
+ ...createRevision(1),
+ description: 'patch 1',
+ _number: 1 as PatchSetNum,
+ },
+ sha2: {
+ ...createRevision(2),
+ description: 'patch 2',
+ _number: 2 as PatchSetNum,
+ },
+ },
+ status: ChangeStatus.NEW,
+ current_revision: 'abc' as CommitId,
+ messages: [],
+ };
+ });
+
+ teardown(() => {
+ testCompleted.next();
+ changeModel.finalize();
+ });
+
+ test('load a change', async () => {
+ const promise = mockPromise<ParsedChangeInfo | undefined>();
+ const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
+ let state: ChangeState | undefined = {
+ loadingStatus: LoadingStatus.NOT_LOADED,
+ };
+ changeModel.state$
+ .pipe(takeUntil(testCompleted))
+ .subscribe(s => (state = s));
+
+ await waitUntil(() => state?.loadingStatus === LoadingStatus.NOT_LOADED);
+ assert.equal(stub.callCount, 0);
+ assert.isUndefined(state?.change);
+
+ changeModel.routerModel.setState({
+ view: GerritView.CHANGE,
+ changeNum: knownChange._number,
+ });
+ await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADING);
+ assert.equal(stub.callCount, 1);
+ assert.isUndefined(state?.change);
+
+ promise.resolve(knownChange);
+ await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADED);
+ assert.equal(stub.callCount, 1);
+ assert.equal(state?.change, knownChange);
+ });
+
+ test('reload a change', async () => {
+ // setting up a loaded change
+ const promise = mockPromise<ParsedChangeInfo | undefined>();
+ const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
+ let state: ChangeState | undefined = {
+ loadingStatus: LoadingStatus.NOT_LOADED,
+ };
+ changeModel.state$
+ .pipe(takeUntil(testCompleted))
+ .subscribe(s => (state = s));
+ changeModel.routerModel.setState({
+ view: GerritView.CHANGE,
+ changeNum: knownChange._number,
+ });
+ promise.resolve(knownChange);
+ await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADED);
+
+ // Reloading same change
+ document.dispatchEvent(new CustomEvent('reload'));
+ await waitUntil(() => state?.loadingStatus === LoadingStatus.RELOADING);
+ assert.equal(stub.callCount, 2);
+ assert.equal(state?.change, knownChange);
+
+ promise.resolve(knownChange);
+ await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADED);
+ assert.equal(stub.callCount, 2);
+ assert.equal(state?.change, knownChange);
+ });
+
+ test('navigating to another change', async () => {
+ // setting up a loaded change
+ let promise = mockPromise<ParsedChangeInfo | undefined>();
+ const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
+ let state: ChangeState | undefined = {
+ loadingStatus: LoadingStatus.NOT_LOADED,
+ };
+ changeModel.state$
+ .pipe(takeUntil(testCompleted))
+ .subscribe(s => (state = s));
+ changeModel.routerModel.setState({
+ view: GerritView.CHANGE,
+ changeNum: knownChange._number,
+ });
+ promise.resolve(knownChange);
+ await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADED);
+
+ // Navigating to other change
+
+ const otherChange: ParsedChangeInfo = {
+ ...knownChange,
+ _number: 123 as NumericChangeId,
+ };
+ promise = mockPromise<ParsedChangeInfo | undefined>();
+ changeModel.routerModel.setState({
+ view: GerritView.CHANGE,
+ changeNum: otherChange._number,
+ });
+ await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADING);
+ assert.equal(stub.callCount, 2);
+ assert.isUndefined(state?.change);
+
+ promise.resolve(otherChange);
+ await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADED);
+ assert.equal(stub.callCount, 2);
+ assert.equal(state?.change, otherChange);
+ });
+
+ test('navigating to dashboard', async () => {
+ // setting up a loaded change
+ let promise = mockPromise<ParsedChangeInfo | undefined>();
+ const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
+ let state: ChangeState | undefined = {
+ loadingStatus: LoadingStatus.NOT_LOADED,
+ };
+ changeModel.state$
+ .pipe(takeUntil(testCompleted))
+ .subscribe(s => (state = s));
+ changeModel.routerModel.setState({
+ view: GerritView.CHANGE,
+ changeNum: knownChange._number,
+ });
+ promise.resolve(knownChange);
+ await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADED);
+
+ // Navigating to dashboard
+
+ promise = mockPromise<ParsedChangeInfo | undefined>();
+ promise.resolve(undefined);
+ changeModel.routerModel.setState({
+ view: GerritView.CHANGE,
+ changeNum: undefined,
+ });
+ await waitUntil(() => state?.loadingStatus === LoadingStatus.NOT_LOADED);
+ assert.equal(stub.callCount, 2);
+ assert.isUndefined(state?.change);
+
+ // Navigating back from dashboard to change page
+
+ promise = mockPromise<ParsedChangeInfo | undefined>();
+ promise.resolve(knownChange);
+ changeModel.routerModel.setState({
+ view: GerritView.CHANGE,
+ changeNum: knownChange._number,
+ });
+ await waitUntil(() => state?.loadingStatus === LoadingStatus.LOADED);
+ assert.equal(stub.callCount, 3);
+ assert.equal(state?.change, knownChange);
+ });
+
+ test('changeModel.fetchChangeUpdates on latest', async () => {
+ stubRestApi('getChangeDetail').returns(Promise.resolve(knownChange));
+ const result = await changeModel.fetchChangeUpdates(knownChange);
+ assert.isTrue(result.isLatest);
+ assert.isNotOk(result.newStatus);
+ assert.isNotOk(result.newMessages);
+ });
+
+ test('changeModel.fetchChangeUpdates not on latest', async () => {
+ const actualChange = {
+ ...knownChange,
+ revisions: {
+ ...knownChange.revisions,
+ sha3: {
+ ...createRevision(3),
+ description: 'patch 3',
+ _number: 3 as PatchSetNum,
+ },
+ },
+ };
+ stubRestApi('getChangeDetail').returns(Promise.resolve(actualChange));
+ const result = await changeModel.fetchChangeUpdates(knownChange);
+ assert.isFalse(result.isLatest);
+ assert.isNotOk(result.newStatus);
+ assert.isNotOk(result.newMessages);
+ });
+
+ test('changeModel.fetchChangeUpdates new status', async () => {
+ const actualChange = {
+ ...knownChange,
+ status: ChangeStatus.MERGED,
+ };
+ stubRestApi('getChangeDetail').returns(Promise.resolve(actualChange));
+ const result = await changeModel.fetchChangeUpdates(knownChange);
+ assert.isTrue(result.isLatest);
+ assert.equal(result.newStatus, ChangeStatus.MERGED);
+ assert.isNotOk(result.newMessages);
+ });
+
+ test('changeModel.fetchChangeUpdates new messages', async () => {
+ const actualChange = {
+ ...knownChange,
+ messages: [{...createChangeMessageInfo(), message: 'blah blah'}],
+ };
+ stubRestApi('getChangeDetail').returns(Promise.resolve(actualChange));
+ const result = await changeModel.fetchChangeUpdates(knownChange);
+ assert.isTrue(result.isLatest);
+ assert.isNotOk(result.newStatus);
+ assert.deepEqual(result.newMessages, {
+ ...createChangeMessageInfo(),
+ message: 'blah blah',
+ });
+ });
+});
diff --git a/polygerrit-ui/app/services/change/change-service.ts b/polygerrit-ui/app/services/change/change-service.ts
deleted file mode 100644
index 0b9a1f2..0000000
--- a/polygerrit-ui/app/services/change/change-service.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {routerChangeNum$} from '../router/router-model';
-import {change$, updateState} from './change-model';
-import {ParsedChangeInfo} from '../../types/types';
-import {appContext} from '../app-context';
-import {ChangeInfo} from '../../types/common';
-import {
- computeAllPatchSets,
- computeLatestPatchNum,
-} from '../../utils/patch-set-util';
-
-export class ChangeService {
- private change?: ParsedChangeInfo;
-
- private readonly restApiService = appContext.restApiService;
-
- constructor() {
- // TODO: In the future we will want to make restApiService.getChangeDetail()
- // calls from a switchMap() here. For now just make sure to invalidate the
- // change when no changeNum is set.
- routerChangeNum$.subscribe(changeNum => {
- if (!changeNum) updateState(undefined);
- });
- change$.subscribe(change => {
- this.change = change;
- });
- }
-
- /**
- * This is a temporary indirection between change-view, which currently
- * manages what the current change is, and the change-model, which will
- * become the source of truth in the future. We will extract a substantial
- * amount of code from change-view and move it into this change-service. This
- * will take some time ...
- */
- updateChange(change: ParsedChangeInfo) {
- updateState(change);
- }
-
- /**
- * Typically you would just subscribe to change$ yourself to get updates. But
- * sometimes it is nice to also be able to get the current ChangeInfo on
- * demand. So here it is for your convenience.
- */
- getChange() {
- return this.change;
- }
-
- /**
- * Check whether there is no newer patch than the latest patch that was
- * available when this change was loaded.
- *
- * @return A promise that yields true if the latest patch
- * has been loaded, and false if a newer patch has been uploaded in the
- * meantime. The promise is rejected on network error.
- */
- fetchChangeUpdates(change: ChangeInfo | ParsedChangeInfo) {
- const knownLatest = computeLatestPatchNum(computeAllPatchSets(change));
- return this.restApiService.getChangeDetail(change._number).then(detail => {
- if (!detail) {
- const error = new Error('Change detail not found.');
- return Promise.reject(error);
- }
- const actualLatest = computeLatestPatchNum(computeAllPatchSets(detail));
- if (!actualLatest || !knownLatest) {
- const error = new Error('Unable to check for latest patchset.');
- return Promise.reject(error);
- }
- return {
- isLatest: actualLatest <= knownLatest,
- newStatus: change.status !== detail.status ? detail.status : null,
- newMessages:
- (change.messages || []).length < (detail.messages || []).length
- ? detail.messages![detail.messages!.length - 1]
- : undefined,
- };
- });
- }
-}
diff --git a/polygerrit-ui/app/services/change/change-services_test.ts b/polygerrit-ui/app/services/change/change-services_test.ts
deleted file mode 100644
index 3e427ff..0000000
--- a/polygerrit-ui/app/services/change/change-services_test.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-/**
- * @license
- * 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.
- */
-
-import {ChangeStatus} from '../../constants/constants';
-import '../../test/common-test-setup-karma';
-import {
- createChange,
- createChangeMessageInfo,
- createRevision,
-} from '../../test/test-data-generators';
-import {stubRestApi} from '../../test/test-utils';
-import {CommitId, PatchSetNum} from '../../types/common';
-import {ParsedChangeInfo} from '../../types/types';
-import {ChangeService} from './change-service';
-
-suite('change service tests', () => {
- let changeService: ChangeService;
- let knownChange: ParsedChangeInfo;
- setup(() => {
- changeService = new ChangeService();
- knownChange = {
- ...createChange(),
- revisions: {
- sha1: {
- ...createRevision(1),
- description: 'patch 1',
- _number: 1 as PatchSetNum,
- },
- sha2: {
- ...createRevision(2),
- description: 'patch 2',
- _number: 2 as PatchSetNum,
- },
- },
- status: ChangeStatus.NEW,
- current_revision: 'abc' as CommitId,
- messages: [],
- };
- });
-
- test('changeService.fetchChangeUpdates on latest', async () => {
- stubRestApi('getChangeDetail').returns(Promise.resolve(knownChange));
- const result = await changeService.fetchChangeUpdates(knownChange);
- assert.isTrue(result.isLatest);
- assert.isNotOk(result.newStatus);
- assert.isNotOk(result.newMessages);
- });
-
- test('changeService.fetchChangeUpdates not on latest', async () => {
- const actualChange = {
- ...knownChange,
- revisions: {
- ...knownChange.revisions,
- sha3: {
- ...createRevision(3),
- description: 'patch 3',
- _number: 3 as PatchSetNum,
- },
- },
- };
- stubRestApi('getChangeDetail').returns(Promise.resolve(actualChange));
- const result = await changeService.fetchChangeUpdates(knownChange);
- assert.isFalse(result.isLatest);
- assert.isNotOk(result.newStatus);
- assert.isNotOk(result.newMessages);
- });
-
- test('changeService.fetchChangeUpdates new status', async () => {
- const actualChange = {
- ...knownChange,
- status: ChangeStatus.MERGED,
- };
- stubRestApi('getChangeDetail').returns(Promise.resolve(actualChange));
- const result = await changeService.fetchChangeUpdates(knownChange);
- assert.isTrue(result.isLatest);
- assert.equal(result.newStatus, ChangeStatus.MERGED);
- assert.isNotOk(result.newMessages);
- });
-
- test('changeService.fetchChangeUpdates new messages', async () => {
- const actualChange = {
- ...knownChange,
- messages: [{...createChangeMessageInfo(), message: 'blah blah'}],
- };
- stubRestApi('getChangeDetail').returns(Promise.resolve(actualChange));
- const result = await changeService.fetchChangeUpdates(knownChange);
- assert.isTrue(result.isLatest);
- assert.isNotOk(result.newStatus);
- assert.deepEqual(result.newMessages, {
- ...createChangeMessageInfo(),
- message: 'blah blah',
- });
- });
-});
diff --git a/polygerrit-ui/app/services/checks/checks-fakes.ts b/polygerrit-ui/app/services/checks/checks-fakes.ts
new file mode 100644
index 0000000..b227db0
--- /dev/null
+++ b/polygerrit-ui/app/services/checks/checks-fakes.ts
@@ -0,0 +1,428 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ Action,
+ Category,
+ Link,
+ LinkIcon,
+ RunStatus,
+ TagColor,
+} from '../../api/checks';
+import {CheckRun} from './checks-model';
+
+// TODO(brohlfs): Eventually these fakes should be removed. But they have proven
+// to be super convenient for testing, debugging and demoing, so I would like to
+// keep them around for a few quarters. Maybe remove by EOY 2022?
+
+export const fakeRun0: CheckRun = {
+ pluginName: 'f0',
+ internalRunId: 'f0',
+ checkName: 'FAKE Error Finder Finder Finder Finder Finder Finder Finder',
+ labelName: 'Presubmit',
+ isSingleAttempt: true,
+ isLatestAttempt: true,
+ attemptDetails: [],
+ results: [
+ {
+ internalResultId: 'f0r0',
+ category: Category.ERROR,
+ summary: 'I would like to point out this error: 1 is not equal to 2!',
+ links: [
+ {primary: true, url: 'https://www.google.com', icon: LinkIcon.EXTERNAL},
+ ],
+ tags: [{name: 'OBSOLETE'}, {name: 'E2E'}],
+ },
+ {
+ internalResultId: 'f0r1',
+ category: Category.ERROR,
+ summary: 'Running the mighty test has failed by crashing.',
+ message: 'Btw, 1 is also not equal to 3. Did you know?',
+ actions: [
+ {
+ name: 'Ignore',
+ tooltip: 'Ignore this result',
+ primary: true,
+ callback: () => Promise.resolve({message: 'fake "ignore" triggered'}),
+ },
+ {
+ name: 'Flag',
+ tooltip: 'Flag this result as totally absolutely really not useful',
+ primary: true,
+ disabled: true,
+ callback: () => Promise.resolve({message: 'flag "flag" triggered'}),
+ },
+ {
+ name: 'Upload',
+ tooltip: 'Upload the result to the super cloud.',
+ primary: false,
+ callback: () => Promise.resolve({message: 'fake "upload" triggered'}),
+ },
+ ],
+ tags: [{name: 'INTERRUPTED', color: TagColor.BROWN}, {name: 'WINDOWS'}],
+ links: [
+ {primary: false, url: 'https://google.com', icon: LinkIcon.EXTERNAL},
+ {primary: true, url: 'https://google.com', icon: LinkIcon.DOWNLOAD},
+ {
+ primary: true,
+ url: 'https://google.com',
+ icon: LinkIcon.DOWNLOAD_MOBILE,
+ },
+ {primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE},
+ {primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE},
+ {primary: false, url: 'https://google.com', icon: LinkIcon.IMAGE},
+ {primary: true, url: 'https://google.com', icon: LinkIcon.REPORT_BUG},
+ {primary: true, url: 'https://google.com', icon: LinkIcon.HELP_PAGE},
+ {primary: true, url: 'https://google.com', icon: LinkIcon.HISTORY},
+ ],
+ },
+ ],
+ status: RunStatus.COMPLETED,
+};
+
+export const fakeRun1: CheckRun = {
+ pluginName: 'f1',
+ internalRunId: 'f1',
+ checkName: 'FAKE Super Check',
+ statusLink: 'https://www.google.com/',
+ patchset: 1,
+ labelName: 'Verified',
+ isSingleAttempt: true,
+ isLatestAttempt: true,
+ attemptDetails: [],
+ results: [
+ {
+ internalResultId: 'f1r0',
+ category: Category.WARNING,
+ summary: 'We think that you could improve this.',
+ message: `There is a lot to be said. A lot. I say, a lot.\n
+ So please keep reading.`,
+ tags: [{name: 'INTERRUPTED', color: TagColor.PURPLE}, {name: 'WINDOWS'}],
+ codePointers: [
+ {
+ path: '/COMMIT_MSG',
+ range: {
+ start_line: 10,
+ start_character: 0,
+ end_line: 10,
+ end_character: 0,
+ },
+ },
+ {
+ path: 'polygerrit-ui/app/api/checks.ts',
+ range: {
+ start_line: 5,
+ start_character: 0,
+ end_line: 7,
+ end_character: 0,
+ },
+ },
+ ],
+ links: [
+ {primary: true, url: 'https://google.com', icon: LinkIcon.EXTERNAL},
+ {primary: true, url: 'https://google.com', icon: LinkIcon.DOWNLOAD},
+ {
+ primary: true,
+ url: 'https://google.com',
+ icon: LinkIcon.DOWNLOAD_MOBILE,
+ },
+ {primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE},
+ {
+ primary: false,
+ url: 'https://google.com',
+ tooltip: 'look at this',
+ icon: LinkIcon.IMAGE,
+ },
+ {
+ primary: false,
+ url: 'https://google.com',
+ tooltip: 'not at this',
+ icon: LinkIcon.IMAGE,
+ },
+ ],
+ },
+ ],
+ status: RunStatus.RUNNING,
+};
+
+export const fakeRun2: CheckRun = {
+ pluginName: 'f2',
+ internalRunId: 'f2',
+ checkName: 'FAKE Mega Analysis',
+ statusDescription: 'This run is nearly completed, but not quite.',
+ statusLink: 'https://www.google.com/',
+ checkDescription:
+ 'From what the title says you can tell that this check analyses.',
+ checkLink: 'https://www.google.com/',
+ scheduledTimestamp: new Date('2021-04-01T03:14:15'),
+ startedTimestamp: new Date('2021-04-01T04:24:25'),
+ finishedTimestamp: new Date('2021-04-01T04:44:44'),
+ isSingleAttempt: true,
+ isLatestAttempt: true,
+ attemptDetails: [],
+ actions: [
+ {
+ name: 'Re-Run',
+ tooltip: 'More powerful run than before',
+ primary: true,
+ callback: () => Promise.resolve({message: 'fake "re-run" triggered'}),
+ },
+ {
+ name: 'Monetize',
+ primary: true,
+ disabled: true,
+ callback: () => Promise.resolve({message: 'fake "monetize" triggered'}),
+ },
+ {
+ name: 'Delete',
+ primary: true,
+ callback: () => Promise.resolve({message: 'fake "delete" triggered'}),
+ },
+ ],
+ results: [
+ {
+ internalResultId: 'f2r0',
+ category: Category.INFO,
+ summary: 'This is looking a bit too large.',
+ message: `We are still looking into how large exactly. Stay tuned.
+And have a look at https://www.google.com!
+
+Or have a look at change 30000.
+Example code:
+ const constable = '';
+ var variable = '';`,
+ tags: [{name: 'FLAKY'}, {name: 'MAC-OS'}],
+ },
+ ],
+ status: RunStatus.COMPLETED,
+};
+
+export const fakeRun3: CheckRun = {
+ pluginName: 'f3',
+ internalRunId: 'f3',
+ checkName: 'FAKE Critical Observations',
+ status: RunStatus.RUNNABLE,
+ isSingleAttempt: true,
+ isLatestAttempt: true,
+ attemptDetails: [],
+};
+
+export const fakeRun4_1: CheckRun = {
+ pluginName: 'f4',
+ internalRunId: 'f4',
+ checkName: 'FAKE Elimination Long Long Long Long Long',
+ status: RunStatus.RUNNABLE,
+ attempt: 1,
+ isSingleAttempt: false,
+ isLatestAttempt: false,
+ attemptDetails: [],
+};
+
+export const fakeRun4_2: CheckRun = {
+ pluginName: 'f4',
+ internalRunId: 'f4',
+ checkName: 'FAKE Elimination Long Long Long Long Long',
+ status: RunStatus.COMPLETED,
+ attempt: 2,
+ isSingleAttempt: false,
+ isLatestAttempt: false,
+ attemptDetails: [],
+ results: [
+ {
+ internalResultId: 'f42r0',
+ category: Category.INFO,
+ summary: 'Please eliminate all the TODOs!',
+ },
+ ],
+};
+
+export const fakeRun4_3: CheckRun = {
+ pluginName: 'f4',
+ internalRunId: 'f4',
+ checkName: 'FAKE Elimination Long Long Long Long Long',
+ status: RunStatus.COMPLETED,
+ attempt: 3,
+ isSingleAttempt: false,
+ isLatestAttempt: false,
+ attemptDetails: [],
+ results: [
+ {
+ internalResultId: 'f43r0',
+ category: Category.ERROR,
+ summary: 'Without eliminating all the TODOs your change will break!',
+ },
+ ],
+};
+
+export const fakeRun4_4: CheckRun = {
+ pluginName: 'f4',
+ internalRunId: 'f4',
+ checkName: 'FAKE Elimination Long Long Long Long Long',
+ checkDescription: 'Shows you the possible eliminations.',
+ checkLink: 'https://www.google.com',
+ status: RunStatus.COMPLETED,
+ statusDescription: 'Everything was eliminated already.',
+ statusLink: 'https://www.google.com',
+ attempt: 40,
+ scheduledTimestamp: new Date('2021-04-02T03:14:15'),
+ startedTimestamp: new Date('2021-04-02T04:24:25'),
+ finishedTimestamp: new Date('2021-04-02T04:25:44'),
+ isSingleAttempt: false,
+ isLatestAttempt: true,
+ attemptDetails: [],
+ results: [
+ {
+ internalResultId: 'f44r0',
+ category: Category.INFO,
+ summary: 'Dont be afraid. All TODOs will be eliminated.',
+ actions: [
+ {
+ name: 'Re-Run',
+ tooltip: 'More powerful run than before with a long tooltip, really.',
+ primary: true,
+ callback: () => Promise.resolve({message: 'fake "re-run" triggered'}),
+ },
+ ],
+ },
+ ],
+ actions: [
+ {
+ name: 'Re-Run',
+ tooltip: 'small',
+ primary: true,
+ callback: () => Promise.resolve({message: 'fake "re-run" triggered'}),
+ },
+ ],
+};
+
+export function fakeRun4CreateAttempts(from: number, to: number): CheckRun[] {
+ const runs: CheckRun[] = [];
+ for (let i = from; i < to; i++) {
+ runs.push(fakeRun4CreateAttempt(i));
+ }
+ return runs;
+}
+
+export function fakeRun4CreateAttempt(attempt: number): CheckRun {
+ return {
+ pluginName: 'f4',
+ internalRunId: 'f4',
+ checkName: 'FAKE Elimination Long Long Long Long Long',
+ status: RunStatus.COMPLETED,
+ attempt,
+ isSingleAttempt: false,
+ isLatestAttempt: false,
+ attemptDetails: [],
+ results:
+ attempt % 2 === 0
+ ? [
+ {
+ internalResultId: 'f43r0',
+ category: Category.ERROR,
+ summary:
+ 'Without eliminating all the TODOs your change will break!',
+ },
+ ]
+ : [],
+ };
+}
+
+export const fakeRun4Att = [
+ fakeRun4_1,
+ fakeRun4_2,
+ fakeRun4_3,
+ ...fakeRun4CreateAttempts(5, 40),
+ fakeRun4_4,
+];
+
+export const fakeActions: Action[] = [
+ {
+ name: 'Fake Action 1',
+ primary: true,
+ disabled: true,
+ tooltip: 'Tooltip for Fake Action 1',
+ callback: () => Promise.resolve({message: 'fake action 1 triggered'}),
+ },
+ {
+ name: 'Fake Action 2',
+ primary: false,
+ disabled: true,
+ tooltip: 'Tooltip for Fake Action 2',
+ callback: () => Promise.resolve({message: 'fake action 2 triggered'}),
+ },
+ {
+ name: 'Fake Action 3',
+ summary: true,
+ primary: false,
+ tooltip: 'Tooltip for Fake Action 3',
+ callback: () => Promise.resolve({message: 'fake action 3 triggered'}),
+ },
+];
+
+export const fakeLinks: Link[] = [
+ {
+ url: 'https://www.google.com',
+ primary: true,
+ tooltip: 'Fake Bug Report 1',
+ icon: LinkIcon.REPORT_BUG,
+ },
+ {
+ url: 'https://www.google.com',
+ primary: true,
+ tooltip: 'Fake Bug Report 2',
+ icon: LinkIcon.REPORT_BUG,
+ },
+ {
+ url: 'https://www.google.com',
+ primary: true,
+ tooltip: 'Fake Link 1',
+ icon: LinkIcon.EXTERNAL,
+ },
+ {
+ url: 'https://www.google.com',
+ primary: false,
+ tooltip: 'Fake Link 2',
+ icon: LinkIcon.EXTERNAL,
+ },
+ {
+ url: 'https://www.google.com',
+ primary: true,
+ tooltip: 'Fake Code Link',
+ icon: LinkIcon.CODE,
+ },
+ {
+ url: 'https://www.google.com',
+ primary: true,
+ tooltip: 'Fake Image Link',
+ icon: LinkIcon.IMAGE,
+ },
+ {
+ url: 'https://www.google.com',
+ primary: true,
+ tooltip: 'Fake Help Link',
+ icon: LinkIcon.HELP_PAGE,
+ },
+];
+
+export const fakeRun5: CheckRun = {
+ pluginName: 'f5',
+ internalRunId: 'f5',
+ checkName: 'FAKE Of Tomorrow',
+ status: RunStatus.SCHEDULED,
+ isSingleAttempt: true,
+ isLatestAttempt: true,
+ attemptDetails: [],
+};
diff --git a/polygerrit-ui/app/services/checks/checks-model.ts b/polygerrit-ui/app/services/checks/checks-model.ts
index 75c24b6d..2bdee51 100644
--- a/polygerrit-ui/app/services/checks/checks-model.ts
+++ b/polygerrit-ui/app/services/checks/checks-model.ts
@@ -14,23 +14,48 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-import {BehaviorSubject, Observable} from 'rxjs';
+import {AttemptDetail, createAttemptMap} from './checks-util';
+import {assertIsDefined} from '../../utils/common-util';
+import {select} from '../../utils/observable-util';
+import {Finalizable} from '../registry';
+import {
+ BehaviorSubject,
+ combineLatest,
+ from,
+ Observable,
+ of,
+ Subject,
+ Subscription,
+ timer,
+} from 'rxjs';
+import {
+ catchError,
+ filter,
+ switchMap,
+ takeUntil,
+ takeWhile,
+ throttleTime,
+} from 'rxjs/operators';
import {
Action,
- Category,
CheckResult as CheckResultApi,
CheckRun as CheckRunApi,
Link,
- LinkIcon,
- RunStatus,
- TagColor,
+ ChangeData,
+ ChecksApiConfig,
+ ChecksProvider,
+ FetchResponse,
+ ResponseCode,
} from '../../api/checks';
-import {distinctUntilChanged, map} from 'rxjs/operators';
-import {PatchSetNumber} from '../../types/common';
-import {AttemptDetail, createAttemptMap} from './checks-util';
-import {assertIsDefined} from '../../utils/common-util';
-import {deepEqualStringDict, equalArray} from '../../utils/compare-util';
+import {ChangeModel} from '../change/change-model';
+import {ChangeInfo, NumericChangeId, PatchSetNumber} from '../../types/common';
+import {getCurrentRevision} from '../../utils/change-util';
+import {getShaByPatchNum} from '../../utils/patch-set-util';
+import {ReportingService} from '../gr-reporting/gr-reporting';
+import {Execution} from '../../constants/reporting';
+import {fireAlert, fireEvent} from '../../utils/event-util';
+import {RouterModel} from '../router/router-model';
+import {Model} from '../../models/model';
/**
* The checks model maintains the state of checks for two patchsets: the latest
@@ -83,7 +108,7 @@
// properties. So you can just combine them with {...run, ...result}.
export type RunResult = CheckRun & CheckResult;
-interface ChecksProviderState {
+export interface ChecksProviderState {
pluginName: string;
loading: boolean;
/**
@@ -121,115 +146,90 @@
};
}
-const initialState: ChecksState = {
- pluginStateLatest: {},
- pluginStateSelected: {},
-};
-
-// Mutable for testing
-let privateState$ = new BehaviorSubject(initialState);
-
-export function _testOnly_resetState() {
- privateState$ = new BehaviorSubject(initialState);
-}
-
-export function _testOnly_setState(state: ChecksState) {
- privateState$.next(state);
-}
-
-export function _testOnly_getState() {
- return privateState$.getValue();
-}
-
-// Re-exporting as Observable so that you can only subscribe, but not emit.
-export const checksState$: Observable<ChecksState> = privateState$;
-
-export const checksSelectedPatchsetNumber$ = checksState$.pipe(
- map(state => state.patchsetNumberSelected),
- distinctUntilChanged()
-);
-
-export const checksLatest$ = checksState$.pipe(
- map(state => state.pluginStateLatest),
- distinctUntilChanged()
-);
-
-export const checksSelected$ = checksState$.pipe(
- map(state =>
- state.patchsetNumberSelected
- ? state.pluginStateSelected
- : state.pluginStateLatest
- ),
- distinctUntilChanged()
-);
-
-export const aPluginHasRegistered$ = checksLatest$.pipe(
- map(state => Object.keys(state).length > 0),
- distinctUntilChanged()
-);
-
-export const someProvidersAreLoadingFirstTime$ = checksLatest$.pipe(
- map(state =>
- Object.values(state).some(
- provider => provider.loading && provider.firstTimeLoad
- )
- ),
- distinctUntilChanged()
-);
-
-export const someProvidersAreLoadingLatest$ = checksLatest$.pipe(
- map(state =>
- Object.values(state).some(providerState => providerState.loading)
- ),
- distinctUntilChanged()
-);
-
-export const someProvidersAreLoadingSelected$ = checksSelected$.pipe(
- map(state =>
- Object.values(state).some(providerState => providerState.loading)
- ),
- distinctUntilChanged()
-);
-
-export const errorMessageLatest$ = checksLatest$.pipe(
- map(
- state =>
- Object.values(state).find(
- providerState => providerState.errorMessage !== undefined
- )?.errorMessage
- ),
- distinctUntilChanged()
-);
-
export interface ErrorMessages {
/* Maps plugin name to error message. */
[name: string]: string;
}
-export const errorMessagesLatest$ = checksLatest$.pipe(
- map(state => {
+export class ChecksModel extends Model<ChecksState> implements Finalizable {
+ private readonly providers: {[name: string]: ChecksProvider} = {};
+
+ private readonly reloadSubjects: {[name: string]: Subject<void>} = {};
+
+ private checkToPluginMap = new Map<string, string>();
+
+ private changeNum?: NumericChangeId;
+
+ private latestPatchNum?: PatchSetNumber;
+
+ private readonly documentVisibilityChange$ = new BehaviorSubject(undefined);
+
+ private readonly reloadListener: () => void;
+
+ private readonly visibilityChangeListener: () => void;
+
+ private subscriptions: Subscription[] = [];
+
+ public checksSelectedPatchsetNumber$ = select(
+ this.state$,
+ state => state.patchsetNumberSelected
+ );
+
+ public checksLatest$ = select(this.state$, state => state.pluginStateLatest);
+
+ public checksSelected$ = select(this.state$, state =>
+ state.patchsetNumberSelected
+ ? state.pluginStateSelected
+ : state.pluginStateLatest
+ );
+
+ public aPluginHasRegistered$ = select(
+ this.checksLatest$,
+ state => Object.keys(state).length > 0
+ );
+
+ public someProvidersAreLoadingFirstTime$ = select(this.checksLatest$, state =>
+ Object.values(state).some(
+ provider => provider.loading && provider.firstTimeLoad
+ )
+ );
+
+ public someProvidersAreLoadingLatest$ = select(this.checksLatest$, state =>
+ Object.values(state).some(providerState => providerState.loading)
+ );
+
+ public someProvidersAreLoadingSelected$ = select(
+ this.checksSelected$,
+ state => Object.values(state).some(providerState => providerState.loading)
+ );
+
+ public errorMessageLatest$ = select(
+ this.checksLatest$,
+
+ state =>
+ Object.values(state).find(
+ providerState => providerState.errorMessage !== undefined
+ )?.errorMessage
+ );
+
+ public errorMessagesLatest$ = select(this.checksLatest$, state => {
const errorMessages: ErrorMessages = {};
for (const providerState of Object.values(state)) {
if (providerState.errorMessage === undefined) continue;
errorMessages[providerState.pluginName] = providerState.errorMessage;
}
return errorMessages;
- }),
- distinctUntilChanged(deepEqualStringDict)
-);
+ });
-export const loginCallbackLatest$ = checksLatest$.pipe(
- map(
+ public loginCallbackLatest$ = select(
+ this.checksLatest$,
state =>
Object.values(state).find(
providerState => providerState.loginCallback !== undefined
)?.loginCallback
- ),
- distinctUntilChanged()
-);
+ );
-export const topLevelActionsLatest$ = checksLatest$.pipe(
- map(state =>
+ public topLevelActionsLatest$ = select(this.checksLatest$, state =>
Object.values(state).reduce(
(allActions: Action[], providerState: ChecksProviderState) => [
...allActions,
@@ -237,12 +237,9 @@
],
[]
)
- ),
- distinctUntilChanged<Action[]>(equalArray)
-);
+ );
-export const topLevelActionsSelected$ = checksSelected$.pipe(
- map(state =>
+ public topLevelActionsSelected$ = select(this.checksSelected$, state =>
Object.values(state).reduce(
(allActions: Action[], providerState: ChecksProviderState) => [
...allActions,
@@ -250,12 +247,9 @@
],
[]
)
- ),
- distinctUntilChanged<Action[]>(equalArray)
-);
+ );
-export const topLevelLinksSelected$ = checksSelected$.pipe(
- map(state =>
+ public topLevelLinksSelected$ = select(this.checksSelected$, state =>
Object.values(state).reduce(
(allLinks: Link[], providerState: ChecksProviderState) => [
...allLinks,
@@ -263,12 +257,9 @@
],
[]
)
- ),
- distinctUntilChanged<Link[]>(equalArray)
-);
+ );
-export const allRunsLatestPatchset$ = checksLatest$.pipe(
- map(state =>
+ public allRunsLatestPatchset$ = select(this.checksLatest$, state =>
Object.values(state).reduce(
(allRuns: CheckRun[], providerState: ChecksProviderState) => [
...allRuns,
@@ -276,12 +267,9 @@
],
[]
)
- ),
- distinctUntilChanged<CheckRun[]>(equalArray)
-);
+ );
-export const allRunsSelectedPatchset$ = checksSelected$.pipe(
- map(state =>
+ public allRunsSelectedPatchset$ = select(this.checksSelected$, state =>
Object.values(state).reduce(
(allRuns: CheckRun[], providerState: ChecksProviderState) => [
...allRuns,
@@ -289,16 +277,14 @@
],
[]
)
- ),
- distinctUntilChanged<CheckRun[]>(equalArray)
-);
+ );
-export const allRunsLatestPatchsetLatestAttempt$ = allRunsLatestPatchset$.pipe(
- map(runs => runs.filter(run => run.isLatestAttempt))
-);
+ public allRunsLatestPatchsetLatestAttempt$ = select(
+ this.allRunsLatestPatchset$,
+ runs => runs.filter(run => run.isLatestAttempt)
+ );
-export const checkToPluginMap$ = checksLatest$.pipe(
- map(state => {
+ public checkToPluginMap$ = select(this.checksLatest$, state => {
const map = new Map<string, string>();
for (const [pluginName, providerState] of Object.entries(state)) {
for (const run of providerState.runs) {
@@ -306,11 +292,9 @@
}
}
return map;
- })
-);
+ });
-export const allResultsSelected$ = checksSelected$.pipe(
- map(state =>
+ public allResultsSelected$ = select(this.checksSelected$, state =>
Object.values(state)
.reduce(
(allResults: CheckResult[], providerState: ChecksProviderState) => [
@@ -324,569 +308,442 @@
[]
)
.filter(r => r !== undefined)
- )
-);
+ );
-// Must only be used by the checks service or whatever is in control of this
-// model.
-export function updateStateSetProvider(
- pluginName: string,
- patchset: ChecksPatchset
-) {
- const nextState = {...privateState$.getValue()};
- const pluginState = getPluginState(nextState, patchset);
- pluginState[pluginName] = {
- pluginName,
- loading: false,
- firstTimeLoad: true,
- runs: [],
- actions: [],
- links: [],
- };
- privateState$.next(nextState);
-}
-
-// TODO(brohlfs): Remove all fake runs once the Checks UI is fully launched.
-// They are just making it easier to develop the UI and always see all the
-// different types/states of runs and results.
-
-export const fakeRun0: CheckRun = {
- pluginName: 'f0',
- internalRunId: 'f0',
- checkName: 'FAKE Error Finder Finder Finder Finder Finder Finder Finder',
- labelName: 'Presubmit',
- isSingleAttempt: true,
- isLatestAttempt: true,
- attemptDetails: [],
- results: [
- {
- internalResultId: 'f0r0',
- category: Category.ERROR,
- summary: 'I would like to point out this error: 1 is not equal to 2!',
- links: [
- {primary: true, url: 'https://www.google.com', icon: LinkIcon.EXTERNAL},
- ],
- tags: [{name: 'OBSOLETE'}, {name: 'E2E'}],
- },
- {
- internalResultId: 'f0r1',
- category: Category.ERROR,
- summary: 'Running the mighty test has failed by crashing.',
- message: 'Btw, 1 is also not equal to 3. Did you know?',
- actions: [
- {
- name: 'Ignore',
- tooltip: 'Ignore this result',
- primary: true,
- callback: () => Promise.resolve({message: 'fake "ignore" triggered'}),
- },
- {
- name: 'Flag',
- tooltip: 'Flag this result as totally absolutely really not useful',
- primary: true,
- disabled: true,
- callback: () => Promise.resolve({message: 'flag "flag" triggered'}),
- },
- {
- name: 'Upload',
- tooltip: 'Upload the result to the super cloud.',
- primary: false,
- callback: () => Promise.resolve({message: 'fake "upload" triggered'}),
- },
- ],
- tags: [{name: 'INTERRUPTED', color: TagColor.BROWN}, {name: 'WINDOWS'}],
- links: [
- {primary: false, url: 'https://google.com', icon: LinkIcon.EXTERNAL},
- {primary: true, url: 'https://google.com', icon: LinkIcon.DOWNLOAD},
- {
- primary: true,
- url: 'https://google.com',
- icon: LinkIcon.DOWNLOAD_MOBILE,
- },
- {primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE},
- {primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE},
- {primary: false, url: 'https://google.com', icon: LinkIcon.IMAGE},
- {primary: true, url: 'https://google.com', icon: LinkIcon.REPORT_BUG},
- {primary: true, url: 'https://google.com', icon: LinkIcon.HELP_PAGE},
- {primary: true, url: 'https://google.com', icon: LinkIcon.HISTORY},
- ],
- },
- ],
- status: RunStatus.COMPLETED,
-};
-
-export const fakeRun1: CheckRun = {
- pluginName: 'f1',
- internalRunId: 'f1',
- checkName: 'FAKE Super Check',
- statusLink: 'https://www.google.com/',
- patchset: 1,
- labelName: 'Verified',
- isSingleAttempt: true,
- isLatestAttempt: true,
- attemptDetails: [],
- results: [
- {
- internalResultId: 'f1r0',
- category: Category.WARNING,
- summary: 'We think that you could improve this.',
- message: `There is a lot to be said. A lot. I say, a lot.\n
- So please keep reading.`,
- tags: [{name: 'INTERRUPTED', color: TagColor.PURPLE}, {name: 'WINDOWS'}],
- codePointers: [
- {
- path: '/COMMIT_MSG',
- range: {
- start_line: 10,
- start_character: 0,
- end_line: 10,
- end_character: 0,
- },
- },
- {
- path: 'polygerrit-ui/app/api/checks.ts',
- range: {
- start_line: 5,
- start_character: 0,
- end_line: 7,
- end_character: 0,
- },
- },
- ],
- links: [
- {primary: true, url: 'https://google.com', icon: LinkIcon.EXTERNAL},
- {primary: true, url: 'https://google.com', icon: LinkIcon.DOWNLOAD},
- {
- primary: true,
- url: 'https://google.com',
- icon: LinkIcon.DOWNLOAD_MOBILE,
- },
- {primary: true, url: 'https://google.com', icon: LinkIcon.IMAGE},
- {
- primary: false,
- url: 'https://google.com',
- tooltip: 'look at this',
- icon: LinkIcon.IMAGE,
- },
- {
- primary: false,
- url: 'https://google.com',
- tooltip: 'not at this',
- icon: LinkIcon.IMAGE,
- },
- ],
- },
- ],
- status: RunStatus.RUNNING,
-};
-
-export const fakeRun2: CheckRun = {
- pluginName: 'f2',
- internalRunId: 'f2',
- checkName: 'FAKE Mega Analysis',
- statusDescription: 'This run is nearly completed, but not quite.',
- statusLink: 'https://www.google.com/',
- checkDescription:
- 'From what the title says you can tell that this check analyses.',
- checkLink: 'https://www.google.com/',
- scheduledTimestamp: new Date('2021-04-01T03:14:15'),
- startedTimestamp: new Date('2021-04-01T04:24:25'),
- finishedTimestamp: new Date('2021-04-01T04:44:44'),
- isSingleAttempt: true,
- isLatestAttempt: true,
- attemptDetails: [],
- actions: [
- {
- name: 'Re-Run',
- tooltip: 'More powerful run than before',
- primary: true,
- callback: () => Promise.resolve({message: 'fake "re-run" triggered'}),
- },
- {
- name: 'Monetize',
- primary: true,
- disabled: true,
- callback: () => Promise.resolve({message: 'fake "monetize" triggered'}),
- },
- {
- name: 'Delete',
- primary: true,
- callback: () => Promise.resolve({message: 'fake "delete" triggered'}),
- },
- ],
- results: [
- {
- internalResultId: 'f2r0',
- category: Category.INFO,
- summary: 'This is looking a bit too large.',
- message: `We are still looking into how large exactly. Stay tuned.
-And have a look at https://www.google.com!
-
-Or have a look at change 30000.
-Example code:
- const constable = '';
- var variable = '';`,
- tags: [{name: 'FLAKY'}, {name: 'MAC-OS'}],
- },
- ],
- status: RunStatus.COMPLETED,
-};
-
-export const fakeRun3: CheckRun = {
- pluginName: 'f3',
- internalRunId: 'f3',
- checkName: 'FAKE Critical Observations',
- status: RunStatus.RUNNABLE,
- isSingleAttempt: true,
- isLatestAttempt: true,
- attemptDetails: [],
-};
-
-export const fakeRun4_1: CheckRun = {
- pluginName: 'f4',
- internalRunId: 'f4',
- checkName: 'FAKE Elimination Long Long Long Long Long',
- status: RunStatus.RUNNABLE,
- attempt: 1,
- isSingleAttempt: false,
- isLatestAttempt: false,
- attemptDetails: [],
-};
-
-export const fakeRun4_2: CheckRun = {
- pluginName: 'f4',
- internalRunId: 'f4',
- checkName: 'FAKE Elimination Long Long Long Long Long',
- status: RunStatus.COMPLETED,
- attempt: 2,
- isSingleAttempt: false,
- isLatestAttempt: false,
- attemptDetails: [],
- results: [
- {
- internalResultId: 'f42r0',
- category: Category.INFO,
- summary: 'Please eliminate all the TODOs!',
- },
- ],
-};
-
-export const fakeRun4_3: CheckRun = {
- pluginName: 'f4',
- internalRunId: 'f4',
- checkName: 'FAKE Elimination Long Long Long Long Long',
- status: RunStatus.COMPLETED,
- attempt: 3,
- isSingleAttempt: false,
- isLatestAttempt: false,
- attemptDetails: [],
- results: [
- {
- internalResultId: 'f43r0',
- category: Category.ERROR,
- summary: 'Without eliminating all the TODOs your change will break!',
- },
- ],
-};
-
-export const fakeRun4_4: CheckRun = {
- pluginName: 'f4',
- internalRunId: 'f4',
- checkName: 'FAKE Elimination Long Long Long Long Long',
- checkDescription: 'Shows you the possible eliminations.',
- checkLink: 'https://www.google.com',
- status: RunStatus.COMPLETED,
- statusDescription: 'Everything was eliminated already.',
- statusLink: 'https://www.google.com',
- attempt: 40,
- scheduledTimestamp: new Date('2021-04-02T03:14:15'),
- startedTimestamp: new Date('2021-04-02T04:24:25'),
- finishedTimestamp: new Date('2021-04-02T04:25:44'),
- isSingleAttempt: false,
- isLatestAttempt: true,
- attemptDetails: [],
- results: [
- {
- internalResultId: 'f44r0',
- category: Category.INFO,
- summary: 'Dont be afraid. All TODOs will be eliminated.',
- actions: [
- {
- name: 'Re-Run',
- tooltip: 'More powerful run than before with a long tooltip, really.',
- primary: true,
- callback: () => Promise.resolve({message: 'fake "re-run" triggered'}),
- },
- ],
- },
- ],
- actions: [
- {
- name: 'Re-Run',
- tooltip: 'small',
- primary: true,
- callback: () => Promise.resolve({message: 'fake "re-run" triggered'}),
- },
- ],
-};
-
-export function fakeRun4CreateAttempts(from: number, to: number): CheckRun[] {
- const runs: CheckRun[] = [];
- for (let i = from; i < to; i++) {
- runs.push(fakeRun4CreateAttempt(i));
- }
- return runs;
-}
-
-export function fakeRun4CreateAttempt(attempt: number): CheckRun {
- return {
- pluginName: 'f4',
- internalRunId: 'f4',
- checkName: 'FAKE Elimination Long Long Long Long Long',
- status: RunStatus.COMPLETED,
- attempt,
- isSingleAttempt: false,
- isLatestAttempt: false,
- attemptDetails: [],
- results:
- attempt % 2 === 0
- ? [
- {
- internalResultId: 'f43r0',
- category: Category.ERROR,
- summary:
- 'Without eliminating all the TODOs your change will break!',
- },
- ]
- : [],
- };
-}
-
-export const fakeRun4Att = [
- fakeRun4_1,
- fakeRun4_2,
- fakeRun4_3,
- ...fakeRun4CreateAttempts(5, 40),
- fakeRun4_4,
-];
-
-export const fakeActions: Action[] = [
- {
- name: 'Fake Action 1',
- primary: true,
- disabled: true,
- tooltip: 'Tooltip for Fake Action 1',
- callback: () => Promise.resolve({message: 'fake action 1 triggered'}),
- },
- {
- name: 'Fake Action 2',
- primary: false,
- disabled: true,
- tooltip: 'Tooltip for Fake Action 2',
- callback: () => Promise.resolve({message: 'fake action 2 triggered'}),
- },
- {
- name: 'Fake Action 3',
- summary: true,
- primary: false,
- tooltip: 'Tooltip for Fake Action 3',
- callback: () => Promise.resolve({message: 'fake action 3 triggered'}),
- },
-];
-
-export const fakeLinks: Link[] = [
- {
- url: 'https://www.google.com',
- primary: true,
- tooltip: 'Fake Bug Report 1',
- icon: LinkIcon.REPORT_BUG,
- },
- {
- url: 'https://www.google.com',
- primary: true,
- tooltip: 'Fake Bug Report 2',
- icon: LinkIcon.REPORT_BUG,
- },
- {
- url: 'https://www.google.com',
- primary: true,
- tooltip: 'Fake Link 1',
- icon: LinkIcon.EXTERNAL,
- },
- {
- url: 'https://www.google.com',
- primary: false,
- tooltip: 'Fake Link 2',
- icon: LinkIcon.EXTERNAL,
- },
- {
- url: 'https://www.google.com',
- primary: true,
- tooltip: 'Fake Code Link',
- icon: LinkIcon.CODE,
- },
- {
- url: 'https://www.google.com',
- primary: true,
- tooltip: 'Fake Image Link',
- icon: LinkIcon.IMAGE,
- },
- {
- url: 'https://www.google.com',
- primary: true,
- tooltip: 'Fake Help Link',
- icon: LinkIcon.HELP_PAGE,
- },
-];
-
-export function getPluginState(
- state: ChecksState,
- patchset: ChecksPatchset = ChecksPatchset.LATEST
-) {
- if (patchset === ChecksPatchset.LATEST) {
- state.pluginStateLatest = {...state.pluginStateLatest};
- return state.pluginStateLatest;
- } else {
- state.pluginStateSelected = {...state.pluginStateSelected};
- return state.pluginStateSelected;
- }
-}
-
-export function updateStateSetLoading(
- pluginName: string,
- patchset: ChecksPatchset
-) {
- const nextState = {...privateState$.getValue()};
- const pluginState = getPluginState(nextState, patchset);
- pluginState[pluginName] = {
- ...pluginState[pluginName],
- loading: true,
- };
- privateState$.next(nextState);
-}
-
-export function updateStateSetError(
- pluginName: string,
- errorMessage: string,
- patchset: ChecksPatchset
-) {
- const nextState = {...privateState$.getValue()};
- const pluginState = getPluginState(nextState, patchset);
- pluginState[pluginName] = {
- ...pluginState[pluginName],
- loading: false,
- firstTimeLoad: false,
- errorMessage,
- loginCallback: undefined,
- runs: [],
- actions: [],
- };
- privateState$.next(nextState);
-}
-
-export function updateStateSetNotLoggedIn(
- pluginName: string,
- loginCallback: () => void,
- patchset: ChecksPatchset
-) {
- const nextState = {...privateState$.getValue()};
- const pluginState = getPluginState(nextState, patchset);
- pluginState[pluginName] = {
- ...pluginState[pluginName],
- loading: false,
- firstTimeLoad: false,
- errorMessage: undefined,
- loginCallback,
- runs: [],
- actions: [],
- };
- privateState$.next(nextState);
-}
-
-export function updateStateSetResults(
- pluginName: string,
- runs: CheckRunApi[],
- actions: Action[] = [],
- links: Link[] = [],
- patchset: ChecksPatchset
-) {
- const attemptMap = createAttemptMap(runs);
- for (const attemptInfo of attemptMap.values()) {
- // Per run only one attempt can be undefined, so the '?? -1' is not really
- // relevant for sorting.
- attemptInfo.attempts.sort((a, b) => (a.attempt ?? -1) - (b.attempt ?? -1));
- }
- const nextState = {...privateState$.getValue()};
- const pluginState = getPluginState(nextState, patchset);
- pluginState[pluginName] = {
- ...pluginState[pluginName],
- loading: false,
- firstTimeLoad: false,
- errorMessage: undefined,
- loginCallback: undefined,
- runs: runs.map(run => {
- const runId = `${run.checkName}-${run.change}-${run.patchset}-${run.attempt}`;
- const attemptInfo = attemptMap.get(run.checkName);
- assertIsDefined(attemptInfo, 'attemptInfo');
- return {
- ...run,
- pluginName,
- internalRunId: runId,
- isLatestAttempt: attemptInfo.latestAttempt === run.attempt,
- isSingleAttempt: attemptInfo.isSingleAttempt,
- attemptDetails: attemptInfo.attempts,
- results: (run.results ?? []).map((result, i) => {
- return {
- ...result,
- internalResultId: `${runId}-${i}`,
- };
- }),
- };
- }),
- actions: [...actions],
- links: [...links],
- };
- privateState$.next(nextState);
-}
-
-export function updateStateUpdateResult(
- pluginName: string,
- updatedRun: CheckRunApi,
- updatedResult: CheckResultApi,
- patchset: ChecksPatchset
-) {
- const nextState = {...privateState$.getValue()};
- const pluginState = getPluginState(nextState, patchset);
- let runUpdated = false;
- const runs: CheckRun[] = pluginState[pluginName].runs.map(run => {
- if (run.change !== updatedRun.change) return run;
- if (run.patchset !== updatedRun.patchset) return run;
- if (run.attempt !== updatedRun.attempt) return run;
- if (run.checkName !== updatedRun.checkName) return run;
- let resultUpdated = false;
- const results: CheckResult[] = (run.results ?? []).map(result => {
- if (result.externalId && result.externalId === updatedResult.externalId) {
- runUpdated = true;
- resultUpdated = true;
- return {
- ...updatedResult,
- internalResultId: result.internalResultId,
- };
- }
- return result;
+ constructor(
+ readonly routerModel: RouterModel,
+ readonly changeModel: ChangeModel,
+ readonly reporting: ReportingService
+ ) {
+ super({
+ pluginStateLatest: {},
+ pluginStateSelected: {},
});
- return resultUpdated ? {...run, results} : run;
- });
- if (!runUpdated) return;
- pluginState[pluginName] = {
- ...pluginState[pluginName],
- runs,
- };
- privateState$.next(nextState);
-}
+ this.subscriptions = [
+ this.changeModel.changeNum$.subscribe(x => (this.changeNum = x)),
+ this.checkToPluginMap$.subscribe(map => {
+ this.checkToPluginMap = map;
+ }),
+ combineLatest([
+ this.routerModel.routerPatchNum$,
+ this.changeModel.latestPatchNum$,
+ ]).subscribe(([routerPs, latestPs]) => {
+ this.latestPatchNum = latestPs;
+ if (latestPs === undefined) {
+ this.setPatchset(undefined);
+ } else if (typeof routerPs === 'number') {
+ this.setPatchset(routerPs as PatchSetNumber);
+ } else {
+ this.setPatchset(latestPs);
+ }
+ }),
+ ];
+ this.visibilityChangeListener = () => {
+ this.documentVisibilityChange$.next(undefined);
+ };
+ document.addEventListener(
+ 'visibilitychange',
+ this.visibilityChangeListener
+ );
+ this.reloadListener = () => this.reloadAll();
+ document.addEventListener('reload', this.reloadListener);
+ }
-export function updateStateSetPatchset(patchsetNumber?: PatchSetNumber) {
- const nextState = {...privateState$.getValue()};
- nextState.patchsetNumberSelected = patchsetNumber;
- privateState$.next(nextState);
+ finalize() {
+ document.removeEventListener('reload', this.reloadListener);
+ document.removeEventListener(
+ 'visibilitychange',
+ this.visibilityChangeListener
+ );
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
+ this.subscriptions = [];
+ this.subject$.complete();
+ }
+
+ // Must only be used by the checks service or whatever is in control of this
+ // model.
+ updateStateSetProvider(pluginName: string, patchset: ChecksPatchset) {
+ const nextState = {...this.subject$.getValue()};
+ const pluginState = this.getPluginState(nextState, patchset);
+ pluginState[pluginName] = {
+ pluginName,
+ loading: false,
+ firstTimeLoad: true,
+ runs: [],
+ actions: [],
+ links: [],
+ };
+ this.subject$.next(nextState);
+ }
+
+ getPluginState(
+ state: ChecksState,
+ patchset: ChecksPatchset = ChecksPatchset.LATEST
+ ) {
+ if (patchset === ChecksPatchset.LATEST) {
+ state.pluginStateLatest = {...state.pluginStateLatest};
+ return state.pluginStateLatest;
+ } else {
+ state.pluginStateSelected = {...state.pluginStateSelected};
+ return state.pluginStateSelected;
+ }
+ }
+
+ updateStateSetLoading(pluginName: string, patchset: ChecksPatchset) {
+ const nextState = {...this.subject$.getValue()};
+ const pluginState = this.getPluginState(nextState, patchset);
+ pluginState[pluginName] = {
+ ...pluginState[pluginName],
+ loading: true,
+ };
+ this.subject$.next(nextState);
+ }
+
+ updateStateSetError(
+ pluginName: string,
+ errorMessage: string,
+ patchset: ChecksPatchset
+ ) {
+ const nextState = {...this.subject$.getValue()};
+ const pluginState = this.getPluginState(nextState, patchset);
+ pluginState[pluginName] = {
+ ...pluginState[pluginName],
+ loading: false,
+ firstTimeLoad: false,
+ errorMessage,
+ loginCallback: undefined,
+ runs: [],
+ actions: [],
+ };
+ this.subject$.next(nextState);
+ }
+
+ updateStateSetNotLoggedIn(
+ pluginName: string,
+ loginCallback: () => void,
+ patchset: ChecksPatchset
+ ) {
+ const nextState = {...this.subject$.getValue()};
+ const pluginState = this.getPluginState(nextState, patchset);
+ pluginState[pluginName] = {
+ ...pluginState[pluginName],
+ loading: false,
+ firstTimeLoad: false,
+ errorMessage: undefined,
+ loginCallback,
+ runs: [],
+ actions: [],
+ };
+ this.subject$.next(nextState);
+ }
+
+ updateStateSetResults(
+ pluginName: string,
+ runs: CheckRunApi[],
+ actions: Action[] = [],
+ links: Link[] = [],
+ patchset: ChecksPatchset
+ ) {
+ const attemptMap = createAttemptMap(runs);
+ for (const attemptInfo of attemptMap.values()) {
+ // Per run only one attempt can be undefined, so the '?? -1' is not really
+ // relevant for sorting.
+ attemptInfo.attempts.sort(
+ (a, b) => (a.attempt ?? -1) - (b.attempt ?? -1)
+ );
+ }
+ const nextState = {...this.subject$.getValue()};
+ const pluginState = this.getPluginState(nextState, patchset);
+ pluginState[pluginName] = {
+ ...pluginState[pluginName],
+ loading: false,
+ firstTimeLoad: false,
+ errorMessage: undefined,
+ loginCallback: undefined,
+ runs: runs.map(run => {
+ const runId = `${run.checkName}-${run.change}-${run.patchset}-${run.attempt}`;
+ const attemptInfo = attemptMap.get(run.checkName);
+ assertIsDefined(attemptInfo, 'attemptInfo');
+ return {
+ ...run,
+ pluginName,
+ internalRunId: runId,
+ isLatestAttempt: attemptInfo.latestAttempt === run.attempt,
+ isSingleAttempt: attemptInfo.isSingleAttempt,
+ attemptDetails: attemptInfo.attempts,
+ results: (run.results ?? []).map((result, i) => {
+ return {
+ ...result,
+ internalResultId: `${runId}-${i}`,
+ };
+ }),
+ };
+ }),
+ actions: [...actions],
+ links: [...links],
+ };
+ this.subject$.next(nextState);
+ }
+
+ updateStateUpdateResult(
+ pluginName: string,
+ updatedRun: CheckRunApi,
+ updatedResult: CheckResultApi,
+ patchset: ChecksPatchset
+ ) {
+ const nextState = {...this.subject$.getValue()};
+ const pluginState = this.getPluginState(nextState, patchset);
+ let runUpdated = false;
+ const runs: CheckRun[] = pluginState[pluginName].runs.map(run => {
+ if (run.change !== updatedRun.change) return run;
+ if (run.patchset !== updatedRun.patchset) return run;
+ if (run.attempt !== updatedRun.attempt) return run;
+ if (run.checkName !== updatedRun.checkName) return run;
+ let resultUpdated = false;
+ const results: CheckResult[] = (run.results ?? []).map(result => {
+ if (
+ result.externalId &&
+ result.externalId === updatedResult.externalId
+ ) {
+ runUpdated = true;
+ resultUpdated = true;
+ return {
+ ...updatedResult,
+ internalResultId: result.internalResultId,
+ };
+ }
+ return result;
+ });
+ return resultUpdated ? {...run, results} : run;
+ });
+ if (!runUpdated) return;
+ pluginState[pluginName] = {
+ ...pluginState[pluginName],
+ runs,
+ };
+ this.subject$.next(nextState);
+ }
+
+ updateStateSetPatchset(patchsetNumber?: PatchSetNumber) {
+ const nextState = {...this.subject$.getValue()};
+ nextState.patchsetNumberSelected = patchsetNumber;
+ this.subject$.next(nextState);
+ }
+
+ setPatchset(num?: PatchSetNumber) {
+ this.updateStateSetPatchset(num === this.latestPatchNum ? undefined : num);
+ }
+
+ reload(pluginName: string) {
+ this.reloadSubjects[pluginName].next();
+ }
+
+ reloadAll() {
+ for (const key of Object.keys(this.providers)) {
+ this.reload(key);
+ }
+ }
+
+ reloadForCheck(checkName?: string) {
+ if (!checkName) return;
+ const plugin = this.checkToPluginMap.get(checkName);
+ if (plugin) this.reload(plugin);
+ }
+
+ updateResult(pluginName: string, run: CheckRunApi, result: CheckResultApi) {
+ this.updateStateUpdateResult(
+ pluginName,
+ run,
+ result,
+ ChecksPatchset.LATEST
+ );
+ this.updateStateUpdateResult(
+ pluginName,
+ run,
+ result,
+ ChecksPatchset.SELECTED
+ );
+ }
+
+ triggerAction(action?: Action, run?: CheckRun) {
+ if (!action?.callback) return;
+ if (!this.changeNum) return;
+ const patchSet = run?.patchset ?? this.latestPatchNum;
+ if (!patchSet) return;
+ const promise = action.callback(
+ this.changeNum,
+ patchSet,
+ run?.attempt,
+ run?.externalId,
+ run?.checkName,
+ action.name
+ );
+ // If plugins return undefined or not a promise, then show no toast.
+ if (!promise?.then) return;
+
+ fireAlert(document, `Triggering action '${action.name}' ...`);
+ from(promise)
+ // If the action takes longer than 5 seconds, then most likely the
+ // user is either not interested or the result not relevant anymore.
+ .pipe(takeUntil(timer(5000)))
+ .subscribe(result => {
+ if (result.errorMessage || result.message) {
+ fireAlert(document, `${result.message ?? result.errorMessage}`);
+ } else {
+ fireEvent(document, 'hide-alert');
+ }
+ if (result.shouldReload) {
+ this.reloadForCheck(run?.checkName);
+ }
+ });
+ }
+
+ register(
+ pluginName: string,
+ provider: ChecksProvider,
+ config: ChecksApiConfig
+ ) {
+ if (this.providers[pluginName]) {
+ console.warn(
+ `Plugin '${pluginName}' was trying to register twice as a Checks UI provider. Ignored.`
+ );
+ return;
+ }
+ this.providers[pluginName] = provider;
+ this.reloadSubjects[pluginName] = new BehaviorSubject<void>(undefined);
+ this.updateStateSetProvider(pluginName, ChecksPatchset.LATEST);
+ this.updateStateSetProvider(pluginName, ChecksPatchset.SELECTED);
+ this.initFetchingOfData(pluginName, config, ChecksPatchset.LATEST);
+ this.initFetchingOfData(pluginName, config, ChecksPatchset.SELECTED);
+ }
+
+ initFetchingOfData(
+ pluginName: string,
+ config: ChecksApiConfig,
+ patchset: ChecksPatchset
+ ) {
+ const pollIntervalMs = (config?.fetchPollingIntervalSeconds ?? 60) * 1000;
+ // Various events should trigger fetching checks from the provider:
+ // 1. Change number and patchset number changes.
+ // 2. Specific reload requests.
+ // 3. Regular polling starting with an initial fetch right now.
+ // 4. A hidden Gerrit tab becoming visible.
+ this.subscriptions.push(
+ combineLatest([
+ this.changeModel.change$,
+ patchset === ChecksPatchset.LATEST
+ ? this.changeModel.latestPatchNum$
+ : this.checksSelectedPatchsetNumber$,
+ this.reloadSubjects[pluginName].pipe(throttleTime(1000)),
+ timer(0, pollIntervalMs),
+ this.documentVisibilityChange$,
+ ])
+ .pipe(
+ takeWhile(_ => !!this.providers[pluginName]),
+ filter(_ => document.visibilityState !== 'hidden'),
+ switchMap(([change, patchNum]): Observable<FetchResponse> => {
+ if (!change || !patchNum) return of(this.empty());
+ if (typeof patchNum !== 'number') return of(this.empty());
+ assertIsDefined(change.revisions, 'change.revisions');
+ const patchsetSha = getShaByPatchNum(change.revisions, patchNum);
+ // Sometimes patchNum is updated earlier than change, so change
+ // revisions don't have patchNum yet
+ if (!patchsetSha) return of(this.empty());
+ const data: ChangeData = {
+ changeNumber: change?._number,
+ patchsetNumber: patchNum,
+ patchsetSha,
+ repo: change.project,
+ commitMessage: getCurrentRevision(change)?.commit?.message,
+ changeInfo: change as ChangeInfo,
+ };
+ return this.fetchResults(pluginName, data, patchset);
+ }),
+ catchError(e => {
+ // This should not happen and is really severe, because it means that
+ // the Observable has terminated and we won't recover from that. No
+ // further attempts to fetch results for this plugin will be made.
+ this.reporting.error(e, `checks-model crash for ${pluginName}`);
+ return of(this.createErrorResponse(pluginName, e));
+ })
+ )
+ .subscribe(response => {
+ switch (response.responseCode) {
+ case ResponseCode.ERROR: {
+ const message = response.errorMessage ?? '-';
+ this.reporting.reportExecution(Execution.CHECKS_API_ERROR, {
+ plugin: pluginName,
+ message,
+ });
+ this.updateStateSetError(pluginName, message, patchset);
+ break;
+ }
+ case ResponseCode.NOT_LOGGED_IN: {
+ assertIsDefined(response.loginCallback, 'loginCallback');
+ this.reporting.reportExecution(
+ Execution.CHECKS_API_NOT_LOGGED_IN,
+ {
+ plugin: pluginName,
+ }
+ );
+ this.updateStateSetNotLoggedIn(
+ pluginName,
+ response.loginCallback,
+ patchset
+ );
+ break;
+ }
+ case ResponseCode.OK: {
+ this.updateStateSetResults(
+ pluginName,
+ response.runs ?? [],
+ response.actions ?? [],
+ response.links ?? [],
+ patchset
+ );
+ break;
+ }
+ }
+ })
+ );
+ }
+
+ private empty(): FetchResponse {
+ return {
+ responseCode: ResponseCode.OK,
+ runs: [],
+ };
+ }
+
+ private createErrorResponse(
+ pluginName: string,
+ message: object
+ ): FetchResponse {
+ return {
+ responseCode: ResponseCode.ERROR,
+ errorMessage:
+ `Error message from plugin '${pluginName}':` +
+ ` ${JSON.stringify(message)}`,
+ };
+ }
+
+ private fetchResults(
+ pluginName: string,
+ data: ChangeData,
+ patchset: ChecksPatchset
+ ): Observable<FetchResponse> {
+ this.updateStateSetLoading(pluginName, patchset);
+ const timer = this.reporting.getTimer('ChecksPluginFetch');
+ const fetchPromise = this.providers[pluginName]
+ .fetch(data)
+ .then(response => {
+ timer.end({pluginName});
+ return response;
+ });
+ return from(fetchPromise).pipe(
+ catchError(e => of(this.createErrorResponse(pluginName, e)))
+ );
+ }
}
diff --git a/polygerrit-ui/app/services/checks/checks-model_test.ts b/polygerrit-ui/app/services/checks/checks-model_test.ts
index dbd3f86..1bb0f8a 100644
--- a/polygerrit-ui/app/services/checks/checks-model_test.ts
+++ b/polygerrit-ui/app/services/checks/checks-model_test.ts
@@ -16,16 +16,19 @@
*/
import '../../test/common-test-setup-karma';
import './checks-model';
+import {ChecksModel, ChecksPatchset, ChecksProviderState} from './checks-model';
import {
- _testOnly_getState,
- _testOnly_resetState,
- ChecksPatchset,
- updateStateSetLoading,
- updateStateSetProvider,
- updateStateSetResults,
- updateStateUpdateResult,
-} from './checks-model';
-import {Category, CheckRun, RunStatus} from '../../api/checks';
+ Category,
+ CheckRun,
+ ChecksApiConfig,
+ ChecksProvider,
+ ResponseCode,
+ RunStatus,
+} from '../../api/checks';
+import {getAppContext} from '../app-context';
+import {createParsedChange} from '../../test/test-data-generators';
+import {waitUntil, waitUntilCalled} from '../../test/test-utils';
+import {ParsedChangeInfo} from '../../types/types';
const PLUGIN_NAME = 'test-plugin';
@@ -46,15 +49,56 @@
},
];
-function current() {
- return _testOnly_getState().pluginStateLatest[PLUGIN_NAME];
+const CONFIG: ChecksApiConfig = {
+ fetchPollingIntervalSeconds: 1000,
+};
+
+function createProvider(): ChecksProvider {
+ return {
+ fetch: () =>
+ Promise.resolve({
+ responseCode: ResponseCode.OK,
+ runs: [],
+ }),
+ };
}
suite('checks-model tests', () => {
- test('updateStateSetProvider', () => {
- _testOnly_resetState();
- updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST);
- assert.deepEqual(current(), {
+ let model: ChecksModel;
+
+ let current: ChecksProviderState;
+
+ setup(() => {
+ model = new ChecksModel(
+ getAppContext().routerModel,
+ getAppContext().changeModel,
+ getAppContext().reportingService
+ );
+ model.checksLatest$.subscribe(c => (current = c[PLUGIN_NAME]));
+ });
+
+ teardown(() => {
+ model.finalize();
+ });
+
+ test('register and fetch', async () => {
+ let change: ParsedChangeInfo | undefined = undefined;
+ model.changeModel.change$.subscribe(c => (change = c));
+ const provider = createProvider();
+ const fetchSpy = sinon.spy(provider, 'fetch');
+
+ model.register('test-plugin', provider, CONFIG);
+ await waitUntil(() => change === undefined);
+
+ const testChange = createParsedChange();
+ model.changeModel.updateStateChange(testChange);
+ await waitUntil(() => change === testChange);
+ await waitUntilCalled(fetchSpy, 'fetch');
+ });
+
+ test('model.updateStateSetProvider', () => {
+ model.updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST);
+ assert.deepEqual(current, {
pluginName: PLUGIN_NAME,
loading: false,
firstTimeLoad: true,
@@ -65,48 +109,69 @@
});
test('loading and first time load', () => {
- _testOnly_resetState();
- updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST);
- assert.isFalse(current().loading);
- assert.isTrue(current().firstTimeLoad);
- updateStateSetLoading(PLUGIN_NAME, ChecksPatchset.LATEST);
- assert.isTrue(current().loading);
- assert.isTrue(current().firstTimeLoad);
- updateStateSetResults(PLUGIN_NAME, RUNS, [], [], ChecksPatchset.LATEST);
- assert.isFalse(current().loading);
- assert.isFalse(current().firstTimeLoad);
- updateStateSetLoading(PLUGIN_NAME, ChecksPatchset.LATEST);
- assert.isTrue(current().loading);
- assert.isFalse(current().firstTimeLoad);
- updateStateSetResults(PLUGIN_NAME, RUNS, [], [], ChecksPatchset.LATEST);
- assert.isFalse(current().loading);
- assert.isFalse(current().firstTimeLoad);
+ model.updateStateSetProvider(PLUGIN_NAME, ChecksPatchset.LATEST);
+ assert.isFalse(current.loading);
+ assert.isTrue(current.firstTimeLoad);
+ model.updateStateSetLoading(PLUGIN_NAME, ChecksPatchset.LATEST);
+ assert.isTrue(current.loading);
+ assert.isTrue(current.firstTimeLoad);
+ model.updateStateSetResults(
+ PLUGIN_NAME,
+ RUNS,
+ [],
+ [],
+ ChecksPatchset.LATEST
+ );
+ assert.isFalse(current.loading);
+ assert.isFalse(current.firstTimeLoad);
+ model.updateStateSetLoading(PLUGIN_NAME, ChecksPatchset.LATEST);
+ assert.isTrue(current.loading);
+ assert.isFalse(current.firstTimeLoad);
+ model.updateStateSetResults(
+ PLUGIN_NAME,
+ RUNS,
+ [],
+ [],
+ ChecksPatchset.LATEST
+ );
+ assert.isFalse(current.loading);
+ assert.isFalse(current.firstTimeLoad);
});
- test('updateStateSetResults', () => {
- _testOnly_resetState();
- updateStateSetResults(PLUGIN_NAME, RUNS, [], [], ChecksPatchset.LATEST);
- assert.lengthOf(current().runs, 1);
- assert.lengthOf(current().runs[0].results!, 1);
+ test('model.updateStateSetResults', () => {
+ model.updateStateSetResults(
+ PLUGIN_NAME,
+ RUNS,
+ [],
+ [],
+ ChecksPatchset.LATEST
+ );
+ assert.lengthOf(current.runs, 1);
+ assert.lengthOf(current.runs[0].results!, 1);
});
- test('updateStateUpdateResult', () => {
- _testOnly_resetState();
- updateStateSetResults(PLUGIN_NAME, RUNS, [], [], ChecksPatchset.LATEST);
+ test('model.updateStateUpdateResult', () => {
+ model.updateStateSetResults(
+ PLUGIN_NAME,
+ RUNS,
+ [],
+ [],
+ ChecksPatchset.LATEST
+ );
assert.equal(
- current().runs[0].results![0].summary,
+ current.runs[0].results![0].summary,
RUNS[0]!.results![0].summary
);
const result = RUNS[0].results![0];
const updatedResult = {...result, summary: 'new'};
- updateStateUpdateResult(
+ model.updateStateUpdateResult(
PLUGIN_NAME,
RUNS[0],
updatedResult,
ChecksPatchset.LATEST
);
- assert.lengthOf(current().runs, 1);
- assert.lengthOf(current().runs[0].results!, 1);
- assert.equal(current().runs[0].results![0].summary, 'new');
+ assert.lengthOf(current.runs, 1);
+ assert.lengthOf(current.runs[0].results!, 1);
+ assert.equal(current.runs[0].results![0].summary, 'new');
});
});
diff --git a/polygerrit-ui/app/services/checks/checks-service.ts b/polygerrit-ui/app/services/checks/checks-service.ts
deleted file mode 100644
index 5ebc13c..0000000
--- a/polygerrit-ui/app/services/checks/checks-service.ts
+++ /dev/null
@@ -1,305 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {
- catchError,
- filter,
- switchMap,
- takeUntil,
- takeWhile,
- throttleTime,
- withLatestFrom,
-} from 'rxjs/operators';
-import {
- Action,
- ChangeData,
- CheckResult,
- CheckRun,
- ChecksApiConfig,
- ChecksProvider,
- FetchResponse,
- ResponseCode,
-} from '../../api/checks';
-import {change$, changeNum$, latestPatchNum$} from '../change/change-model';
-import {
- ChecksPatchset,
- checksSelectedPatchsetNumber$,
- checkToPluginMap$,
- updateStateSetError,
- updateStateSetLoading,
- updateStateSetNotLoggedIn,
- updateStateSetPatchset,
- updateStateSetProvider,
- updateStateSetResults,
- updateStateUpdateResult,
-} from './checks-model';
-import {
- BehaviorSubject,
- combineLatest,
- from,
- Observable,
- of,
- Subject,
- timer,
-} from 'rxjs';
-import {ChangeInfo, NumericChangeId, PatchSetNumber} from '../../types/common';
-import {getCurrentRevision} from '../../utils/change-util';
-import {getShaByPatchNum} from '../../utils/patch-set-util';
-import {assertIsDefined} from '../../utils/common-util';
-import {ReportingService} from '../gr-reporting/gr-reporting';
-import {routerPatchNum$} from '../router/router-model';
-import {Execution} from '../../constants/reporting';
-import {fireAlert, fireEvent} from '../../utils/event-util';
-
-export class ChecksService {
- private readonly providers: {[name: string]: ChecksProvider} = {};
-
- private readonly reloadSubjects: {[name: string]: Subject<void>} = {};
-
- private checkToPluginMap = new Map<string, string>();
-
- private changeNum?: NumericChangeId;
-
- private latestPatchNum?: PatchSetNumber;
-
- private readonly documentVisibilityChange$ = new BehaviorSubject(undefined);
-
- constructor(readonly reporting: ReportingService) {
- changeNum$.subscribe(x => (this.changeNum = x));
- checkToPluginMap$.subscribe(map => {
- this.checkToPluginMap = map;
- });
- combineLatest([routerPatchNum$, latestPatchNum$]).subscribe(
- ([routerPs, latestPs]) => {
- this.latestPatchNum = latestPs;
- if (latestPs === undefined) {
- this.setPatchset(undefined);
- } else if (typeof routerPs === 'number') {
- this.setPatchset(routerPs);
- } else {
- this.setPatchset(latestPs);
- }
- }
- );
- document.addEventListener('visibilitychange', () => {
- this.documentVisibilityChange$.next(undefined);
- });
- document.addEventListener('reload', () => {
- this.reloadAll();
- });
- }
-
- setPatchset(num?: PatchSetNumber) {
- updateStateSetPatchset(num === this.latestPatchNum ? undefined : num);
- }
-
- reload(pluginName: string) {
- this.reloadSubjects[pluginName].next();
- }
-
- reloadAll() {
- Object.keys(this.providers).forEach(key => this.reload(key));
- }
-
- reloadForCheck(checkName?: string) {
- if (!checkName) return;
- const plugin = this.checkToPluginMap.get(checkName);
- if (plugin) this.reload(plugin);
- }
-
- updateResult(pluginName: string, run: CheckRun, result: CheckResult) {
- updateStateUpdateResult(pluginName, run, result, ChecksPatchset.LATEST);
- updateStateUpdateResult(pluginName, run, result, ChecksPatchset.SELECTED);
- }
-
- triggerAction(action?: Action, run?: CheckRun) {
- if (!action?.callback) return;
- if (!this.changeNum) return;
- const patchSet = run?.patchset ?? this.latestPatchNum;
- if (!patchSet) return;
- const promise = action.callback(
- this.changeNum,
- patchSet,
- run?.attempt,
- run?.externalId,
- run?.checkName,
- action.name
- );
- // If plugins return undefined or not a promise, then show no toast.
- if (!promise?.then) return;
-
- fireAlert(document, `Triggering action '${action.name}' ...`);
- from(promise)
- // If the action takes longer than 5 seconds, then most likely the
- // user is either not interested or the result not relevant anymore.
- .pipe(takeUntil(timer(5000)))
- .subscribe(result => {
- if (result.errorMessage || result.message) {
- fireAlert(document, `${result.message ?? result.errorMessage}`);
- } else {
- fireEvent(document, 'hide-alert');
- }
- if (result.shouldReload) {
- this.reloadForCheck(run?.checkName);
- }
- });
- }
-
- register(
- pluginName: string,
- provider: ChecksProvider,
- config: ChecksApiConfig
- ) {
- if (this.providers[pluginName]) {
- console.warn(
- `Plugin '${pluginName}' was trying to register twice as a Checks UI provider. Ignored.`
- );
- return;
- }
- this.providers[pluginName] = provider;
- this.reloadSubjects[pluginName] = new BehaviorSubject<void>(undefined);
- updateStateSetProvider(pluginName, ChecksPatchset.LATEST);
- updateStateSetProvider(pluginName, ChecksPatchset.SELECTED);
- this.initFetchingOfData(pluginName, config, ChecksPatchset.LATEST);
- this.initFetchingOfData(pluginName, config, ChecksPatchset.SELECTED);
- }
-
- initFetchingOfData(
- pluginName: string,
- config: ChecksApiConfig,
- patchset: ChecksPatchset
- ) {
- const pollIntervalMs = (config?.fetchPollingIntervalSeconds ?? 60) * 1000;
- // Various events should trigger fetching checks from the provider:
- // 1. Change number and patchset number changes.
- // 2. Specific reload requests.
- // 3. Regular polling starting with an initial fetch right now.
- // 4. A hidden Gerrit tab becoming visible.
- combineLatest([
- changeNum$,
- patchset === ChecksPatchset.LATEST
- ? latestPatchNum$
- : checksSelectedPatchsetNumber$,
- this.reloadSubjects[pluginName].pipe(throttleTime(1000)),
- timer(0, pollIntervalMs),
- this.documentVisibilityChange$,
- ])
- .pipe(
- takeWhile(_ => !!this.providers[pluginName]),
- filter(_ => document.visibilityState !== 'hidden'),
- withLatestFrom(change$),
- switchMap(
- ([[changeNum, patchNum], change]): Observable<FetchResponse> => {
- if (!change || !changeNum || !patchNum) return of(this.empty());
- if (typeof patchNum !== 'number') return of(this.empty());
- assertIsDefined(change.revisions, 'change.revisions');
- const patchsetSha = getShaByPatchNum(change.revisions, patchNum);
- // Sometimes patchNum is updated earlier than change, so change
- // revisions don't have patchNum yet
- if (!patchsetSha) return of(this.empty());
- const data: ChangeData = {
- changeNumber: changeNum,
- patchsetNumber: patchNum,
- patchsetSha,
- repo: change.project,
- commitMessage: getCurrentRevision(change)?.commit?.message,
- changeInfo: change as ChangeInfo,
- };
- return this.fetchResults(pluginName, data, patchset);
- }
- ),
- catchError(e => {
- // This should not happen and is really severe, because it means that
- // the Observable has terminated and we won't recover from that. No
- // further attempts to fetch results for this plugin will be made.
- this.reporting.error(e, `checks-service crash for ${pluginName}`);
- return of(this.createErrorResponse(pluginName, e));
- })
- )
- .subscribe(response => {
- switch (response.responseCode) {
- case ResponseCode.ERROR: {
- const message = response.errorMessage ?? '-';
- this.reporting.reportExecution(Execution.CHECKS_API_ERROR, {
- plugin: pluginName,
- message,
- });
- updateStateSetError(pluginName, message, patchset);
- break;
- }
- case ResponseCode.NOT_LOGGED_IN: {
- assertIsDefined(response.loginCallback, 'loginCallback');
- this.reporting.reportExecution(Execution.CHECKS_API_NOT_LOGGED_IN, {
- plugin: pluginName,
- });
- updateStateSetNotLoggedIn(
- pluginName,
- response.loginCallback,
- patchset
- );
- break;
- }
- case ResponseCode.OK: {
- updateStateSetResults(
- pluginName,
- response.runs ?? [],
- response.actions ?? [],
- response.links ?? [],
- patchset
- );
- break;
- }
- }
- });
- }
-
- private empty(): FetchResponse {
- return {
- responseCode: ResponseCode.OK,
- runs: [],
- };
- }
-
- private createErrorResponse(
- pluginName: string,
- message: object
- ): FetchResponse {
- return {
- responseCode: ResponseCode.ERROR,
- errorMessage:
- `Error message from plugin '${pluginName}':` +
- ` ${JSON.stringify(message)}`,
- };
- }
-
- private fetchResults(
- pluginName: string,
- data: ChangeData,
- patchset: ChecksPatchset
- ): Observable<FetchResponse> {
- updateStateSetLoading(pluginName, patchset);
- const timer = this.reporting.getTimer('ChecksPluginFetch');
- const fetchPromise = this.providers[pluginName]
- .fetch(data)
- .then(response => {
- timer.end({pluginName});
- return response;
- });
- return from(fetchPromise).pipe(
- catchError(e => of(this.createErrorResponse(pluginName, e)))
- );
- }
-}
diff --git a/polygerrit-ui/app/services/checks/checks-util.ts b/polygerrit-ui/app/services/checks/checks-util.ts
index 18cc076..76970e2 100644
--- a/polygerrit-ui/app/services/checks/checks-util.ts
+++ b/polygerrit-ui/app/services/checks/checks-util.ts
@@ -107,6 +107,7 @@
return (
catStat === RunStatus.COMPLETED ||
catStat === RunStatus.RUNNABLE ||
+ catStat === RunStatus.SCHEDULED ||
catStat === RunStatus.RUNNING
);
}
@@ -127,6 +128,8 @@
return 'runnable';
case RunStatus.RUNNING:
return 'running';
+ case RunStatus.SCHEDULED:
+ return 'scheduled';
default:
assertNever(catStat, `Unsupported category/status: ${catStat}`);
}
@@ -149,6 +152,8 @@
return 'placeholder';
case RunStatus.RUNNING:
return 'timelapse';
+ case RunStatus.SCHEDULED:
+ return 'scheduled';
default:
assertNever(catStat, `Unsupported category/status: ${catStat}`);
}
@@ -175,6 +180,8 @@
return 'Not run';
case RunStatus.RUNNING:
return 'Running';
+ case RunStatus.SCHEDULED:
+ return 'Scheduled';
default:
assertNever(status, `Unsupported status: ${status}`);
}
@@ -187,6 +194,7 @@
case RunStatus.RUNNABLE:
return PRIMARY_STATUS_ACTIONS.RUN;
case RunStatus.RUNNING:
+ case RunStatus.SCHEDULED:
return undefined;
default:
assertNever(status, `Unsupported status: ${status}`);
@@ -218,12 +226,16 @@
return run.status === RunStatus.COMPLETED;
}
-export function isRunning(run: CheckRun) {
- return run.status === RunStatus.RUNNING;
+export function isRunningOrScheduled(run: CheckRun) {
+ return run.status === RunStatus.RUNNING || run.status === RunStatus.SCHEDULED;
}
-export function isRunningOrHasCompleted(run: CheckRun) {
- return run.status === RunStatus.COMPLETED || run.status === RunStatus.RUNNING;
+export function isRunningScheduledOrCompleted(run: CheckRun) {
+ return (
+ run.status === RunStatus.COMPLETED ||
+ run.status === RunStatus.RUNNING ||
+ run.status === RunStatus.SCHEDULED
+ );
}
export function hasCompletedWithoutResults(run: CheckRun) {
@@ -257,10 +269,13 @@
}
export function compareByWorstCategory(a: CheckRun, b: CheckRun) {
- return level(worstCategory(b)) - level(worstCategory(a));
+ const catComp = catLevel(worstCategory(b)) - catLevel(worstCategory(a));
+ if (catComp !== 0) return catComp;
+ const statusComp = runLevel(b.status) - runLevel(a.status);
+ return statusComp;
}
-export function level(cat?: Category) {
+function catLevel(cat?: Category) {
if (!cat) return -1;
switch (cat) {
case Category.SUCCESS:
@@ -274,6 +289,21 @@
}
}
+function runLevel(status: RunStatus) {
+ switch (status) {
+ case RunStatus.COMPLETED:
+ return 0;
+ case RunStatus.RUNNABLE:
+ return 1;
+ case RunStatus.RUNNING:
+ return 2;
+ case RunStatus.SCHEDULED:
+ return 3;
+ default:
+ assertNever(status, `Unsupported status: ${status}`);
+ }
+}
+
export interface ActionTriggeredEventDetail {
action: Action;
run?: CheckRun;
diff --git a/polygerrit-ui/app/services/comments/comments-model.ts b/polygerrit-ui/app/services/comments/comments-model.ts
deleted file mode 100644
index 850acbc..0000000
--- a/polygerrit-ui/app/services/comments/comments-model.ts
+++ /dev/null
@@ -1,206 +0,0 @@
-/**
- * @license
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {BehaviorSubject, Observable} from 'rxjs';
-import {distinctUntilChanged, map} from 'rxjs/operators';
-import {ChangeComments} from '../../elements/diff/gr-comment-api/gr-comment-api';
-import {
- CommentInfo,
- PathToCommentsInfoMap,
- RobotCommentInfo,
-} from '../../types/common';
-import {addPath, DraftInfo} from '../../utils/comment-util';
-
-interface CommentState {
- comments: PathToCommentsInfoMap;
- robotComments: {[path: string]: RobotCommentInfo[]};
- drafts: {[path: string]: DraftInfo[]};
- portedComments: PathToCommentsInfoMap;
- portedDrafts: PathToCommentsInfoMap;
- /**
- * If a draft is discarded by the user, then we temporarily keep it in this
- * array in case the user decides to Undo the discard operation and bring the
- * draft back. Once restored, the draft is removed from this array.
- */
- discardedDrafts: DraftInfo[];
-}
-
-const initialState: CommentState = {
- comments: {},
- robotComments: {},
- drafts: {},
- portedComments: {},
- portedDrafts: {},
- discardedDrafts: [],
-};
-
-const privateState$ = new BehaviorSubject(initialState);
-
-export function _testOnly_resetState() {
- privateState$.next(initialState);
-}
-
-// Re-exporting as Observable so that you can only subscribe, but not emit.
-export const commentState$: Observable<CommentState> = privateState$;
-
-export function _testOnly_getState() {
- return privateState$.getValue();
-}
-
-export function _testOnly_setState(state: CommentState) {
- privateState$.next(state);
-}
-
-export const drafts$ = commentState$.pipe(
- map(commentState => commentState.drafts),
- distinctUntilChanged()
-);
-
-export const discardedDrafts$ = commentState$.pipe(
- map(commentState => commentState.discardedDrafts),
- distinctUntilChanged()
-);
-
-// Emits a new value even if only a single draft is changed. Components should
-// aim to subsribe to something more specific.
-export const changeComments$ = commentState$.pipe(
- map(
- commentState =>
- new ChangeComments(
- commentState.comments,
- commentState.robotComments,
- commentState.drafts,
- commentState.portedComments,
- commentState.portedDrafts
- )
- ),
- distinctUntilChanged()
-);
-
-function publishState(state: CommentState) {
- privateState$.next(state);
-}
-
-export function updateStateComments(comments?: {
- [path: string]: CommentInfo[];
-}) {
- const nextState = {...privateState$.getValue()};
- nextState.comments = addPath(comments) || {};
- publishState(nextState);
-}
-
-export function updateStateRobotComments(robotComments?: {
- [path: string]: RobotCommentInfo[];
-}) {
- const nextState = {...privateState$.getValue()};
- nextState.robotComments = addPath(robotComments) || {};
- publishState(nextState);
-}
-
-export function updateStateDrafts(drafts?: {[path: string]: DraftInfo[]}) {
- const nextState = {...privateState$.getValue()};
- nextState.drafts = addPath(drafts) || {};
- publishState(nextState);
-}
-
-export function updateStatePortedComments(
- portedComments?: PathToCommentsInfoMap
-) {
- const nextState = {...privateState$.getValue()};
- nextState.portedComments = portedComments || {};
- publishState(nextState);
-}
-
-export function updateStatePortedDrafts(portedDrafts?: PathToCommentsInfoMap) {
- const nextState = {...privateState$.getValue()};
- nextState.portedDrafts = portedDrafts || {};
- publishState(nextState);
-}
-
-export function updateStateAddDiscardedDraft(draft: DraftInfo) {
- const nextState = {...privateState$.getValue()};
- nextState.discardedDrafts = [...nextState.discardedDrafts, draft];
- publishState(nextState);
-}
-
-export function updateStateUndoDiscardedDraft(draftID?: string) {
- const nextState = {...privateState$.getValue()};
- const drafts = [...nextState.discardedDrafts];
- const index = drafts.findIndex(d => d.id === draftID);
- if (index === -1) {
- throw new Error('discarded draft not found');
- }
- drafts.splice(index, 1);
- nextState.discardedDrafts = drafts;
- publishState(nextState);
-}
-
-export function updateStateAddDraft(draft: DraftInfo) {
- const nextState = {...privateState$.getValue()};
- if (!draft.path) throw new Error('draft path undefined');
- nextState.drafts = {...nextState.drafts};
- const drafts = nextState.drafts;
- if (!drafts[draft.path]) drafts[draft.path] = [] as DraftInfo[];
- else drafts[draft.path] = [...drafts[draft.path]];
- const index = drafts[draft.path].findIndex(
- d =>
- (d.__draftID && d.__draftID === draft.__draftID) ||
- (d.id && d.id === draft.id)
- );
- if (index !== -1) {
- drafts[draft.path][index] = draft;
- } else {
- drafts[draft.path].push(draft);
- }
- publishState(nextState);
-}
-
-export function updateStateUpdateDraft(draft: DraftInfo) {
- const nextState = {...privateState$.getValue()};
- if (!draft.path) throw new Error('draft path undefined');
- nextState.drafts = {...nextState.drafts};
- const drafts = nextState.drafts;
- if (!drafts[draft.path])
- throw new Error('draft: trying to edit non-existent draft');
- drafts[draft.path] = [...drafts[draft.path]];
- const index = drafts[draft.path].findIndex(
- d =>
- (d.__draftID && d.__draftID === draft.__draftID) ||
- (d.id && d.id === draft.id)
- );
- if (index === -1) return;
- drafts[draft.path][index] = draft;
- publishState(nextState);
-}
-
-export function updateStateDeleteDraft(draft: DraftInfo) {
- const nextState = {...privateState$.getValue()};
- if (!draft.path) throw new Error('draft path undefined');
- nextState.drafts = {...nextState.drafts};
- const drafts = nextState.drafts;
- const index = (drafts[draft.path] || []).findIndex(
- d =>
- (d.__draftID && d.__draftID === draft.__draftID) ||
- (d.id && d.id === draft.id)
- );
- if (index === -1) return;
- const discardedDraft = drafts[draft.path][index];
- drafts[draft.path] = [...drafts[draft.path]];
- drafts[draft.path].splice(index, 1);
- publishState(nextState);
- updateStateAddDiscardedDraft(discardedDraft);
-}
diff --git a/polygerrit-ui/app/services/comments/comments-model_test.ts b/polygerrit-ui/app/services/comments/comments-model_test.ts
deleted file mode 100644
index e389254..0000000
--- a/polygerrit-ui/app/services/comments/comments-model_test.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * @license
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import '../../test/common-test-setup-karma';
-import {createDraft} from '../../test/test-data-generators';
-import {UrlEncodedCommentId} from '../../types/common';
-import {DraftInfo} from '../../utils/comment-util';
-import './comments-model';
-import {
- updateStateDeleteDraft,
- _testOnly_getState,
- _testOnly_resetState,
- _testOnly_setState,
-} from './comments-model';
-
-suite('comments model tests', () => {
- test('updateStateDeleteDraft', () => {
- _testOnly_resetState();
- const draft = createDraft();
- draft.id = '1' as UrlEncodedCommentId;
- _testOnly_setState({
- comments: {},
- robotComments: {},
- drafts: {
- [draft.path!]: [draft as DraftInfo],
- },
- portedComments: {},
- portedDrafts: {},
- discardedDrafts: [],
- });
- updateStateDeleteDraft(draft);
- assert.deepEqual(_testOnly_getState(), {
- comments: {},
- robotComments: {},
- drafts: {
- 'abc.txt': [],
- },
- portedComments: {},
- portedDrafts: {},
- discardedDrafts: [{...draft}],
- });
- });
-});
diff --git a/polygerrit-ui/app/services/comments/comments-service.ts b/polygerrit-ui/app/services/comments/comments-service.ts
deleted file mode 100644
index 16ee2f7..0000000
--- a/polygerrit-ui/app/services/comments/comments-service.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-/**
- * @license
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {NumericChangeId, PatchSetNum, RevisionId} from '../../types/common';
-import {DraftInfo, UIDraft} from '../../utils/comment-util';
-import {fireAlert} from '../../utils/event-util';
-import {CURRENT} from '../../utils/patch-set-util';
-import {RestApiService} from '../gr-rest-api/gr-rest-api';
-import {
- updateStateAddDraft,
- updateStateDeleteDraft,
- updateStateUpdateDraft,
- updateStateComments,
- updateStateRobotComments,
- updateStateDrafts,
- updateStatePortedComments,
- updateStatePortedDrafts,
- updateStateUndoDiscardedDraft,
- discardedDrafts$,
-} from './comments-model';
-
-export class CommentsService {
- private discardedDrafts?: UIDraft[] = [];
-
- constructor(readonly restApiService: RestApiService) {
- discardedDrafts$.subscribe(
- discardedDrafts => (this.discardedDrafts = discardedDrafts)
- );
- }
-
- /**
- * Load all comments (with drafts and robot comments) for the given change
- * number. The returned promise resolves when the comments have loaded, but
- * does not yield the comment data.
- */
- // TODO(dhruvsri): listen to changeNum changes or reload event to update
- // automatically
- loadAll(changeNum: NumericChangeId, patchNum = CURRENT as RevisionId) {
- const revision = patchNum;
- this.restApiService
- .getDiffComments(changeNum)
- .then(comments => updateStateComments(comments));
- this.restApiService
- .getDiffRobotComments(changeNum)
- .then(robotComments => updateStateRobotComments(robotComments));
- this.restApiService
- .getDiffDrafts(changeNum)
- .then(drafts => updateStateDrafts(drafts));
- this.restApiService
- .getPortedComments(changeNum, revision)
- .then(portedComments => updateStatePortedComments(portedComments));
- this.restApiService
- .getPortedDrafts(changeNum, revision)
- .then(portedDrafts => updateStatePortedDrafts(portedDrafts));
- }
-
- restoreDraft(
- changeNum: NumericChangeId,
- patchNum: PatchSetNum,
- draftID: string
- ) {
- const draft = {...this.discardedDrafts?.find(d => d.id === draftID)};
- if (!draft) throw new Error('discarded draft not found');
- // delete draft ID since we want to treat this as a new draft creation
- delete draft.id;
- this.restApiService
- .saveDiffDraft(changeNum, patchNum, draft)
- .then(result => {
- if (!result.ok) {
- fireAlert(document, 'Unable to restore draft');
- return;
- }
- this.restApiService.getResponseObject(result).then(obj => {
- const resComment = obj as unknown as DraftInfo;
- resComment.patch_set = draft.patch_set;
- updateStateAddDraft(resComment);
- updateStateUndoDiscardedDraft(draftID);
- });
- });
- }
-
- addDraft(draft: DraftInfo) {
- updateStateAddDraft(draft);
- }
-
- cancelDraft(draft: DraftInfo) {
- updateStateUpdateDraft(draft);
- }
-
- editDraft(draft: DraftInfo) {
- updateStateUpdateDraft(draft);
- }
-
- deleteDraft(draft: DraftInfo) {
- updateStateDeleteDraft(draft);
- }
-}
diff --git a/polygerrit-ui/app/services/comments/comments-service_test.ts b/polygerrit-ui/app/services/comments/comments-service_test.ts
deleted file mode 100644
index 604b5c4..0000000
--- a/polygerrit-ui/app/services/comments/comments-service_test.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * @license
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../test/common-test-setup-karma';
-import {
- createComment,
- createFixSuggestionInfo,
-} from '../../test/test-data-generators';
-import {stubRestApi} from '../../test/test-utils';
-import {
- NumericChangeId,
- RobotId,
- RobotRunId,
- Timestamp,
- UrlEncodedCommentId,
-} from '../../types/common';
-import {appContext} from '../app-context';
-import {CommentsService} from './comments-service';
-
-suite('change service tests', () => {
- let commentsService: CommentsService;
-
- test('loads logged-out', () => {
- const changeNum = 1234 as NumericChangeId;
- commentsService = new CommentsService(appContext.restApiService);
- stubRestApi('getLoggedIn').returns(Promise.resolve(false));
- const diffCommentsSpy = stubRestApi('getDiffComments').returns(
- Promise.resolve({
- 'foo.c': [
- {
- ...createComment(),
- id: '123' as UrlEncodedCommentId,
- message: 'Done',
- updated: '2017-02-08 16:40:49' as Timestamp,
- },
- ],
- })
- );
- const diffRobotCommentsSpy = stubRestApi('getDiffRobotComments').returns(
- Promise.resolve({
- 'foo.c': [
- {
- ...createComment(),
- id: '321' as UrlEncodedCommentId,
- message: 'Done',
- updated: '2017-02-08 16:40:49' as Timestamp,
- robot_id: 'robot_1' as RobotId,
- robot_run_id: 'run_1' as RobotRunId,
- properties: {},
- fix_suggestions: [
- createFixSuggestionInfo('fix_1'),
- createFixSuggestionInfo('fix_2'),
- ],
- },
- ],
- })
- );
- const diffDraftsSpy = stubRestApi('getDiffDrafts').returns(
- Promise.resolve({})
- );
-
- commentsService.loadAll(changeNum);
- assert.isTrue(diffCommentsSpy.calledWithExactly(changeNum));
- assert.isTrue(diffRobotCommentsSpy.calledWithExactly(changeNum));
- assert.isTrue(diffDraftsSpy.calledWithExactly(changeNum));
- });
-
- test('loads logged-in', () => {
- const changeNum = 1234 as NumericChangeId;
-
- stubRestApi('getLoggedIn').returns(Promise.resolve(true));
- const diffCommentsSpy = stubRestApi('getDiffComments').returns(
- Promise.resolve({
- 'foo.c': [
- {
- ...createComment(),
- id: '123' as UrlEncodedCommentId,
- message: 'Done',
- updated: '2017-02-08 16:40:49' as Timestamp,
- },
- ],
- })
- );
- const diffRobotCommentsSpy = stubRestApi('getDiffRobotComments').returns(
- Promise.resolve({
- 'foo.c': [
- {
- ...createComment(),
- id: '321' as UrlEncodedCommentId,
- message: 'Done',
- updated: '2017-02-08 16:40:49' as Timestamp,
- robot_id: 'robot_1' as RobotId,
- robot_run_id: 'run_1' as RobotRunId,
- properties: {},
- fix_suggestions: [
- createFixSuggestionInfo('fix_1'),
- createFixSuggestionInfo('fix_2'),
- ],
- },
- ],
- })
- );
- const diffDraftsSpy = stubRestApi('getDiffDrafts').returns(
- Promise.resolve({})
- );
-
- commentsService.loadAll(changeNum);
- assert.isTrue(diffCommentsSpy.calledWithExactly(changeNum));
- assert.isTrue(diffRobotCommentsSpy.calledWithExactly(changeNum));
- assert.isTrue(diffDraftsSpy.calledWithExactly(changeNum));
- });
-});
diff --git a/polygerrit-ui/app/services/config/config-model.ts b/polygerrit-ui/app/services/config/config-model.ts
deleted file mode 100644
index f5e10c5..0000000
--- a/polygerrit-ui/app/services/config/config-model.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * @license
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {ConfigInfo, ServerInfo} from '../../types/common';
-import {BehaviorSubject, Observable} from 'rxjs';
-import {map, distinctUntilChanged} from 'rxjs/operators';
-
-interface ConfigState {
- repoConfig?: ConfigInfo;
- serverConfig?: ServerInfo;
-}
-
-// TODO: Figure out how to best enforce immutability of all states. Use Immer?
-// Use DeepReadOnly?
-const initialState: ConfigState = {};
-
-const privateState$ = new BehaviorSubject(initialState);
-
-// Re-exporting as Observable so that you can only subscribe, but not emit.
-export const configState$: Observable<ConfigState> = privateState$;
-
-export function updateRepoConfig(repoConfig?: ConfigInfo) {
- const current = privateState$.getValue();
- privateState$.next({...current, repoConfig});
-}
-
-export function updateServerConfig(serverConfig?: ServerInfo) {
- const current = privateState$.getValue();
- privateState$.next({...current, serverConfig});
-}
-
-export const repoConfig$ = configState$.pipe(
- map(configState => configState.repoConfig),
- distinctUntilChanged()
-);
-
-export const serverConfig$ = configState$.pipe(
- map(configState => configState.serverConfig),
- distinctUntilChanged()
-);
diff --git a/polygerrit-ui/app/services/config/config-service.ts b/polygerrit-ui/app/services/config/config-service.ts
deleted file mode 100644
index 7cd1538..0000000
--- a/polygerrit-ui/app/services/config/config-service.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * @license
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {updateRepoConfig, updateServerConfig} from './config-model';
-import {repo$} from '../change/change-model';
-import {appContext} from '../app-context';
-import {switchMap} from 'rxjs/operators';
-import {ConfigInfo, RepoName, ServerInfo} from '../../types/common';
-import {from, of} from 'rxjs';
-
-export class ConfigService {
- private readonly restApiService = appContext.restApiService;
-
- constructor() {
- from(this.restApiService.getConfig()).subscribe((config?: ServerInfo) => {
- updateServerConfig(config);
- });
- repo$
- .pipe(
- switchMap((repo?: RepoName) => {
- if (repo === undefined) return of(undefined);
- return from(this.restApiService.getProjectConfig(repo));
- })
- )
- .subscribe((repoConfig?: ConfigInfo) => {
- updateRepoConfig(repoConfig);
- });
- }
-}
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 863f95f..a4ab95c 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -15,7 +15,9 @@
* limitations under the License.
*/
-export interface FlagsService {
+import {Finalizable} from '../registry';
+
+export interface FlagsService extends Finalizable {
isEnabled(experimentId: string): boolean;
enabledExperiments: string[];
}
@@ -27,4 +29,6 @@
NEW_IMAGE_DIFF_UI = 'UiFeature__new_image_diff_ui',
CHECKS_DEVELOPER = 'UiFeature__checks_developer',
SUBMIT_REQUIREMENTS_UI = 'UiFeature__submit_requirements_ui',
+ TOPICS_PAGE = 'UiFeature__topics_page',
+ CHECK_RESULTS_IN_DIFFS = 'UiFeature__check_results_in_diffs',
}
diff --git a/polygerrit-ui/app/services/flags/flags_impl.ts b/polygerrit-ui/app/services/flags/flags_impl.ts
index 18e225b..9767d1a 100644
--- a/polygerrit-ui/app/services/flags/flags_impl.ts
+++ b/polygerrit-ui/app/services/flags/flags_impl.ts
@@ -15,6 +15,7 @@
* limitations under the License.
*/
import {FlagsService} from './flags';
+import {Finalizable} from '../registry';
declare global {
interface Window {
@@ -27,7 +28,7 @@
*
* Provides all related methods / properties regarding on feature flags.
*/
-export class FlagsServiceImplementation implements FlagsService {
+export class FlagsServiceImplementation implements FlagsService, Finalizable {
private readonly _experiments: Set<string>;
constructor() {
@@ -35,6 +36,8 @@
this._experiments = this._loadExperiments();
}
+ finalize() {}
+
isEnabled(experimentId: string): boolean {
return this._experiments.has(experimentId);
}
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth.ts b/polygerrit-ui/app/services/gr-auth/gr-auth.ts
index f7fdadf..ac63d6a 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth.ts
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
+import {Finalizable} from '../registry';
export enum AuthType {
XSRF_TOKEN = 'xsrf_token',
ACCESS_TOKEN = 'access_token',
@@ -45,7 +45,7 @@
headers?: Headers;
}
-export interface AuthService {
+export interface AuthService extends Finalizable {
baseUrl: string;
isAuthed: boolean;
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 c254284..b740d29 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
@@ -16,6 +16,7 @@
*/
import {getBaseUrl} from '../../utils/url-util';
import {EventEmitterService} from '../gr-event-interface/gr-event-interface';
+import {Finalizable} from '../registry';
import {
AuthRequestInit,
AuthService,
@@ -44,7 +45,7 @@
/**
* Auth class.
*/
-export class Auth implements AuthService {
+export class Auth implements AuthService, Finalizable {
// TODO(dmfilippov): Remove Type and Status properties, expose AuthType and
// AuthStatus to API
static TYPE = {
@@ -88,6 +89,8 @@
return getBaseUrl();
}
+ finalize() {}
+
/**
* Returns if user is authed or not.
*/
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 3dbb4c3..e5331f1 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth_mock.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_mock.ts
@@ -40,6 +40,8 @@
return this._status === Auth.STATUS.AUTHED;
}
+ finalize() {}
+
private _setStatus(status: AuthStatus) {
if (this._status === status) return;
if (this._status === AuthStatus.AUTHED) {
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth_test.js b/polygerrit-ui/app/services/gr-auth/gr-auth_test.js
index debba6d..5ae2e59 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth_test.js
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_test.js
@@ -17,20 +17,23 @@
import '../../test/common-test-setup-karma.js';
import {Auth} from './gr-auth_impl.js';
-import {appContext} from '../app-context.js';
+import {getAppContext} from '../app-context.js';
import {stubBaseUrl} from '../../test/test-utils.js';
suite('gr-auth', () => {
let auth;
+ let eventEmitter;
setup(() => {
- auth = new Auth(appContext.eventEmitter);
+ // TODO(poucet): Mock the eventEmitter completely instead of getting it
+ // from appContext.
+ eventEmitter = getAppContext().eventEmitter;
+ auth = new Auth(eventEmitter);
});
suite('Auth class methods', () => {
let fakeFetch;
setup(() => {
- auth = new Auth(appContext.eventEmitter);
fakeFetch = sinon.stub(window, 'fetch');
});
@@ -67,7 +70,6 @@
let fakeFetch;
let clock;
setup(() => {
- auth = new Auth(appContext.eventEmitter);
clock = sinon.useFakeTimers();
fakeFetch = sinon.stub(window, 'fetch');
});
@@ -124,7 +126,7 @@
assert.equal(auth.status, Auth.STATUS.AUTHED);
clock.tick(1000 * 10000);
fakeFetch.returns(Promise.resolve({status: 403}));
- const emitStub = sinon.stub(appContext.eventEmitter, 'emit');
+ const emitStub = sinon.stub(eventEmitter, 'emit');
const authed2 = await auth.authCheck();
assert.isFalse(authed2);
assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
@@ -138,7 +140,7 @@
assert.equal(auth.status, Auth.STATUS.AUTHED);
clock.tick(1000 * 10000);
fakeFetch.returns(Promise.reject(new Error('random error')));
- const emitStub = sinon.stub(appContext.eventEmitter, 'emit');
+ const emitStub = sinon.stub(eventEmitter, 'emit');
const authed2 = await auth.authCheck();
assert.isFalse(authed2);
assert.isTrue(emitStub.called);
@@ -152,7 +154,7 @@
assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
clock.tick(1000 * 10000);
fakeFetch.returns(Promise.resolve({status: 204}));
- const emitStub = sinon.stub(appContext.eventEmitter, 'emit');
+ const emitStub = sinon.stub(eventEmitter, 'emit');
const authed2 = await auth.authCheck();
assert.isTrue(authed2);
assert.isFalse(emitStub.called);
@@ -166,7 +168,7 @@
assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
clock.tick(1000 * 10000);
fakeFetch.returns(Promise.reject(new Error('random error')));
- const emitStub = sinon.stub(appContext.eventEmitter, 'emit');
+ const emitStub = sinon.stub(eventEmitter, 'emit');
const authed2 = await auth.authCheck();
assert.isFalse(authed2);
assert.isFalse(emitStub.called);
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
index e540029..391a32b 100644
--- a/polygerrit-ui/app/services/gr-event-interface/gr-event-interface.ts
+++ b/polygerrit-ui/app/services/gr-event-interface/gr-event-interface.ts
@@ -14,12 +14,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
+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 {
+export interface EventEmitterService extends Finalizable {
/**
* Register an event listener to an event.
*/
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
index d8c5d77..19d2f61 100644
--- 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
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
+import {Finalizable} from '../registry';
import {
EventCallback,
EventEmitterService,
@@ -34,9 +34,13 @@
* }
*
*/
-export class EventEmitter implements EventEmitterService {
+export class EventEmitter implements EventEmitterService, Finalizable {
private _listenersMap = new Map<string, EventCallback[]>();
+ finalize() {
+ this.removeAllListeners();
+ }
+
/**
* Register an event listener to an event.
*/
@@ -123,7 +127,7 @@
*
* @param eventName if not provided, will remove all
*/
- removeAllListeners(eventName: string): void {
+ removeAllListeners(eventName?: string): void {
if (eventName) {
this._listenersMap.set(eventName, []);
} else {
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
index 06f1a0c..518716b 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting.ts
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
+import {Finalizable} from '../registry';
import {NumericChangeId} from '../../types/common';
import {EventDetails} from '../../api/reporting';
import {PluginApi} from '../../api/plugin';
@@ -33,7 +33,7 @@
withMaximum(maximum: number): this;
}
-export interface ReportingService {
+export interface ReportingService extends Finalizable {
reporter(
type: string,
category: string,
@@ -115,11 +115,6 @@
eventName: string | Interaction,
details?: EventDetails
): void;
- /**
- * A draft interaction was started. Update the time-between-draft-actions
- * timer.
- */
- recordDraftInteraction(): void;
reportErrorDialog(message: string): void;
setRepoName(repoName: string): void;
setChangeId(changeId: NumericChangeId): void;
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
index 65a5784..a01e9db 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_impl.ts
@@ -14,13 +14,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {AppContext} from '../app-context';
import {FlagsService} from '../flags/flags';
import {EventValue, ReportingService, Timer} from './gr-reporting';
import {hasOwnProperty} from '../../utils/common-util';
import {NumericChangeId} from '../../types/common';
import {EventDetails} from '../../api/reporting';
import {PluginApi} from '../../api/plugin';
+import {Finalizable} from '../registry';
import {
Execution,
Interaction,
@@ -98,13 +98,9 @@
[Timing.WEB_COMPONENTS_READY]: 0,
};
-const DRAFT_ACTION_TIMER = 'TimeBetweenDraftActions';
-const DRAFT_ACTION_TIMER_MAX = 2 * 60 * 1000; // 2 minutes.
const SLOW_RPC_THRESHOLD = 500;
-export function initErrorReporter(appContext: AppContext) {
- const reportingService = appContext.reportingService;
-
+export function initErrorReporter(reportingService: ReportingService) {
const normalizeError = (err: Error | unknown) => {
if (err instanceof Error) {
return err;
@@ -169,8 +165,7 @@
return {catchErrors};
}
-export function initPerformanceReporter(appContext: AppContext) {
- const reportingService = appContext.reportingService;
+export function initPerformanceReporter(reportingService: ReportingService) {
// PerformanceObserver interface is a browser API.
if (window.PerformanceObserver) {
const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || [];
@@ -196,8 +191,7 @@
}
}
-export function initVisibilityReporter(appContext: AppContext) {
- const reportingService = appContext.reportingService;
+export function initVisibilityReporter(reportingService: ReportingService) {
document.addEventListener('visibilitychange', () => {
reportingService.onVisibilityChange();
});
@@ -277,7 +271,7 @@
type PendingReportInfo = [EventInfo, boolean | undefined];
-export class GrReporting implements ReportingService {
+export class GrReporting implements ReportingService, Finalizable {
private readonly _flagsService: FlagsService;
private readonly _baselines = STARTUP_TIMERS;
@@ -286,10 +280,6 @@
private reportChangeId: NumericChangeId | undefined;
- private timers: {timeBetweenDraftActions: Timer | null} = {
- timeBetweenDraftActions: null,
- };
-
private pending: PendingReportInfo[] = [];
private slowRpcList: SlowRpcCall[] = [];
@@ -328,6 +318,8 @@
);
}
+ finalize() {}
+
/**
* Reporter reports events. Events will be queued if metrics plugin is not
* yet installed.
@@ -857,27 +849,6 @@
this.reportExecution(Execution.PLUGIN_API, {plugin, object, method});
}
- /**
- * A draft interaction was started. Update the time-between-draft-actions
- * Timing.
- */
- recordDraftInteraction() {
- // If there is no timer defined, then this is the first interaction.
- // Set up the timer so that it's ready to record the intervening time when
- // called again.
- const timer = this.timers.timeBetweenDraftActions;
- if (!timer) {
- // Create a timer with a maximum length.
- this.timers.timeBetweenDraftActions = this.getTimer(
- DRAFT_ACTION_TIMER
- ).withMaximum(DRAFT_ACTION_TIMER_MAX);
- return;
- }
-
- // Mark the time and reinitialize the timer.
- timer.end().reset();
- }
-
error(error: Error, errorSource?: string, details?: EventDetails) {
const eventDetails = details ?? {};
const message = `${errorSource ? errorSource + ': ' : ''}${error.message}`;
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts
index 337cf2f..2a5c532 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts
@@ -18,6 +18,7 @@
import {EventDetails} from '../../api/reporting';
import {PluginApi} from '../../api/plugin';
import {Execution, Interaction} from '../../constants/reporting';
+import {Finalizable} from '../registry';
export class MockTimer implements Timer {
end(): this {
@@ -37,7 +38,7 @@
console.info(`ReportingMock.${msg}`);
};
-export const grReportingMock: ReportingService = {
+export const grReportingMock: ReportingService & Finalizable = {
appStarted: () => {},
beforeLocationChanged: () => {},
changeDisplayed: () => {},
@@ -47,6 +48,7 @@
diffViewDisplayed: () => {},
diffViewFullyLoaded: () => {},
fileListDisplayed: () => {},
+ finalize: () => {},
getTimer: () => new MockTimer(),
locationChanged: (page: string) => {
log(`locationChanged: ${page}`);
@@ -57,7 +59,6 @@
pluginLoaded: () => {},
pluginsLoaded: () => {},
pluginsFailed: () => {},
- recordDraftInteraction: () => {},
reporter: () => {},
reportErrorDialog: (message: string) => {
log(`reportErrorDialog: ${message}`);
@@ -65,20 +66,13 @@
error: () => {
log('error');
},
- reportExecution: (id: Execution, details?: EventDetails) => {
- log(`reportExecution '${id}': ${JSON.stringify(details)}`);
- },
- trackApi: (pluginApi: PluginApi, object: string, method: string) => {
- const plugin = pluginApi?.getPluginName() ?? 'unknown';
- log(`trackApi '${plugin}', ${object}, ${method}`);
- },
+ reportExecution: (_id: Execution, _details?: EventDetails) => {},
+ trackApi: (_pluginApi: PluginApi, _object: string, _method: string) => {},
reportExtension: () => {},
reportInteraction: (
- eventName: string | Interaction,
- details?: EventDetails
- ) => {
- log(`reportInteraction '${eventName}': ${JSON.stringify(details)}`);
- },
+ _eventName: string | Interaction,
+ _details?: EventDetails
+ ) => {},
reportLifeCycle: () => {},
reportPluginLifeCycleLog: () => {},
reportPluginInteractionLog: () => {},
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js
index 9b71908..8068dc00 100644
--- a/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.js
@@ -17,7 +17,7 @@
import '../../test/common-test-setup-karma.js';
import {GrReporting, DEFAULT_STARTUP_TIMERS, initErrorReporter} from './gr-reporting_impl.js';
-import {appContext} from '../app-context.js';
+import {getAppContext} from '../app-context.js';
suite('gr-reporting tests', () => {
let service;
@@ -28,7 +28,7 @@
setup(() => {
clock = sinon.useFakeTimers(NOW_TIME);
- service = new GrReporting(appContext.flagsService);
+ service = new GrReporting(getAppContext().flagsService);
service._baselines = {...DEFAULT_STARTUP_TIMERS};
sinon.stub(service, 'reporter');
});
@@ -282,30 +282,6 @@
assert.isTrue(service.reporter.calledOnce);
});
- test('recordDraftInteraction', () => {
- const key = 'TimeBetweenDraftActions';
- const nowStub = sinon.stub(window.performance, 'now').returns(100);
- const timingStub = sinon.stub(service, '_reportTiming');
- service.recordDraftInteraction();
- assert.isFalse(timingStub.called);
-
- nowStub.returns(200);
- service.recordDraftInteraction();
- assert.isTrue(timingStub.calledOnce);
- assert.equal(timingStub.lastCall.args[0], key);
- assert.equal(timingStub.lastCall.args[1], 100);
-
- nowStub.returns(350);
- service.recordDraftInteraction();
- assert.isTrue(timingStub.calledTwice);
- assert.equal(timingStub.lastCall.args[0], key);
- assert.equal(timingStub.lastCall.args[1], 150);
-
- nowStub.returns(370 + 2 * 60 * 1000);
- service.recordDraftInteraction();
- assert.isFalse(timingStub.calledThrice);
- });
-
test('timeEndWithAverage', () => {
const nowStub = sinon.stub(window.performance, 'now').returns(0);
nowStub.returns(1000);
@@ -461,12 +437,12 @@
},
};
sinon.stub(console, 'error');
- Object.defineProperty(appContext, 'reportingService', {
+ Object.defineProperty(getAppContext(), 'reportingService', {
get() {
return service;
},
});
- const errorReporter = initErrorReporter(appContext);
+ const errorReporter = initErrorReporter(getAppContext().reportingService);
errorReporter.catchErrors(fakeWindow);
});
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 445e932..7c35222 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
@@ -16,6 +16,7 @@
*/
import {HttpMethod} from '../../constants/constants';
+import {Finalizable} from '../registry';
import {
AccountCapabilityInfo,
AccountDetailInfo,
@@ -108,17 +109,10 @@
} from '../../types/diff';
import {ParsedChangeInfo} from '../../types/types';
import {ErrorCallback} from '../../api/rest';
+import {DraftInfo} from '../../utils/comment-util';
export type CancelConditionCallback = () => boolean;
-// TODO(TS): remove when GrReplyDialog converted to typescript
-export interface GrReplyDialog {
- getLabelValue(label: string): string;
- setLabelValue(label: string, value: string): void;
- send(includeComments?: boolean, startReview?: boolean): Promise<unknown>;
- setPluginMessage(message: string): void;
-}
-
export interface GetDiffCommentsOutput {
baseComments: CommentInfo[];
comments: CommentInfo[];
@@ -129,7 +123,7 @@
comments: RobotCommentInfo[];
}
-export interface RestApiService {
+export interface RestApiService extends Finalizable {
getConfig(noCache?: boolean): Promise<ServerInfo | undefined>;
getLoggedIn(): Promise<boolean>;
getPreferences(): Promise<PreferencesInfo | undefined>;
@@ -200,10 +194,10 @@
): Promise<BranchInfo[] | undefined>;
getChangeDetail(
- changeNum: number | string,
+ changeNum?: number | string,
opt_errFn?: ErrorCallback,
opt_cancelCondition?: Function
- ): Promise<ParsedChangeInfo | null | undefined>;
+ ): Promise<ParsedChangeInfo | undefined>;
getChange(
changeNum: ChangeId | NumericChangeId,
@@ -325,7 +319,7 @@
getIsAdmin(): Promise<boolean | undefined>;
- getIsGroupOwner(groupName: GroupName): Promise<boolean>;
+ getIsGroupOwner(groupName?: GroupName): Promise<boolean>;
saveGroupName(
groupId: GroupId | GroupName,
@@ -365,10 +359,7 @@
errFn?: ErrorCallback
): Promise<Response>;
- getChangeEdit(
- changeNum: NumericChangeId,
- downloadCommands?: boolean
- ): Promise<false | EditInfo | undefined>;
+ getChangeEdit(changeNum?: NumericChangeId): Promise<EditInfo | undefined>;
getChangeActionURL(
changeNum: NumericChangeId,
@@ -401,10 +392,6 @@
draft: CommentInput
): Promise<Response>;
- getDiffChangeDetail(
- changeNum: NumericChangeId
- ): Promise<ChangeInfo | undefined | null>;
-
getPortedComments(
changeNum: NumericChangeId,
revision: RevisionId
@@ -453,21 +440,7 @@
getDiffDrafts(
changeNum: NumericChangeId
- ): Promise<PathToCommentsInfoMap | undefined>;
- getDiffDrafts(
- changeNum: NumericChangeId,
- basePatchNum: PatchSetNum,
- patchNum: PatchSetNum,
- path: string
- ): Promise<GetDiffCommentsOutput>;
- getDiffDrafts(
- changeNum: NumericChangeId,
- basePatchNum?: BasePatchSetNum,
- patchNum?: PatchSetNum,
- path?: string
- ):
- | Promise<GetDiffCommentsOutput>
- | Promise<PathToCommentsInfoMap | undefined>;
+ ): Promise<{[path: string]: DraftInfo[]} | undefined>;
createGroup(config: GroupInput & {name: string}): Promise<Response>;
@@ -647,7 +620,8 @@
): Promise<RelatedChangesInfo | undefined>;
getChangesSubmittedTogether(
- changeNum: NumericChangeId
+ changeNum: NumericChangeId,
+ options?: string[]
): Promise<SubmittedTogetherInfo | undefined>;
getChangeConflicts(
@@ -662,7 +636,10 @@
getChangesWithSameTopic(
topic: string,
- changeNum: NumericChangeId
+ options?: {
+ openChangesOnly?: boolean;
+ changeToExclude?: NumericChangeId;
+ }
): Promise<ChangeInfo[] | undefined>;
getChangesWithSimilarTopic(topic: string): Promise<ChangeInfo[] | undefined>;
@@ -727,13 +704,6 @@
deleteDraftComments(query: string): Promise<Response>;
- setAssignee(
- changeNum: NumericChangeId,
- assignee: AccountId
- ): Promise<Response>;
-
- deleteAssignee(changeNum: NumericChangeId): Promise<Response>;
-
setChangeHashtag(
changeNum: NumericChangeId,
hashtag: HashtagsInput
diff --git a/polygerrit-ui/app/services/registry.ts b/polygerrit-ui/app/services/registry.ts
new file mode 100644
index 0000000..e7de1ef
--- /dev/null
+++ b/polygerrit-ui/app/services/registry.ts
@@ -0,0 +1,86 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// A finalizable object has a single method `finalize` that is called when
+// the object is no longer needed and should clean itself up.
+export interface Finalizable {
+ finalize(): void;
+}
+
+// A factory can take a partially created TContext and generate a property
+// for a given key on that TContext.
+export type Factory<TContext, K extends keyof TContext> = (
+ ctx: Partial<TContext>
+) => TContext[K] & Finalizable;
+
+// A registry contains a factory for each key in TContext.
+export type Registry<TContext> = {[P in keyof TContext]: Factory<TContext, P>} &
+ Record<string, (_: TContext) => Finalizable>;
+
+// Creates a context given a registry.
+export function create<TContext>(
+ registry: Registry<TContext>
+): TContext & Finalizable {
+ const context: Partial<TContext> & Finalizable = {
+ finalize() {
+ for (const key of Object.getOwnPropertyNames(registry)) {
+ const name = key as keyof TContext;
+ try {
+ if (this[name]) {
+ (this[name] as unknown as Finalizable).finalize();
+ }
+ } catch (e) {
+ console.info(`Failed to finalize ${name}`);
+ throw e;
+ }
+ }
+ },
+ } as Partial<TContext> & Finalizable;
+
+ const initialized: Map<keyof TContext, Finalizable> = new Map<
+ keyof TContext,
+ Finalizable
+ >();
+ for (const key of Object.keys(registry)) {
+ const name = key as keyof TContext;
+ const factory = registry[name];
+ let initializing = false;
+ Object.defineProperty(context, name, {
+ configurable: true, // Tests can mock properties
+ get() {
+ if (!initialized.has(name)) {
+ // Notice that this is the getter for the property in question.
+ // It is possible that during the initialization of one property,
+ // another property is required. This extra check ensures that
+ // the construction of propertiers on Context are not circularly
+ // dependent.
+ if (initializing) throw new Error(`Circular dependency for ${key}`);
+ try {
+ initializing = true;
+ initialized.set(name, factory(context));
+ } catch (e) {
+ console.error(`Failed to initialize ${name}`, e);
+ } finally {
+ initializing = false;
+ }
+ }
+ return initialized.get(name);
+ },
+ });
+ }
+ return context as TContext & Finalizable;
+}
diff --git a/polygerrit-ui/app/services/registry_test.ts b/polygerrit-ui/app/services/registry_test.ts
new file mode 100644
index 0000000..d677be0
--- /dev/null
+++ b/polygerrit-ui/app/services/registry_test.ts
@@ -0,0 +1,57 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {create, Finalizable, Registry} from './registry';
+import '../test/common-test-setup-karma.js';
+
+class Foo implements Finalizable {
+ constructor(private readonly final: string[]) {}
+
+ finalize() {
+ this.final.push('Foo');
+ }
+}
+
+class Bar implements Finalizable {
+ constructor(private readonly final: string[], _foo?: Foo) {}
+
+ finalize() {
+ this.final.push('Bar');
+ }
+}
+
+interface DemoContext {
+ foo: Foo;
+ bar: Bar;
+}
+
+suite('Registry', () => {
+ setup(() => {});
+
+ test('It finalizes correctly', () => {
+ const final: string[] = [];
+ const demoRegistry: Registry<DemoContext> = {
+ foo: (_ctx: Partial<DemoContext>) => new Foo(final),
+ bar: (ctx: Partial<DemoContext>) => new Bar(final, ctx.foo),
+ };
+ const demoContext: DemoContext & Finalizable = create<DemoContext>(
+ demoRegistry
+ ) as DemoContext & Finalizable;
+ demoContext.finalize();
+ assert.deepEqual(final, ['Foo', 'Bar']);
+ });
+});
diff --git a/polygerrit-ui/app/services/router/router-model.ts b/polygerrit-ui/app/services/router/router-model.ts
index b3cdf9e..5bd228b 100644
--- a/polygerrit-ui/app/services/router/router-model.ts
+++ b/polygerrit-ui/app/services/router/router-model.ts
@@ -15,9 +15,11 @@
* limitations under the License.
*/
-import {NumericChangeId, PatchSetNum} from '../../types/common';
-import {BehaviorSubject, Observable} from 'rxjs';
+import {Observable} from 'rxjs';
import {distinctUntilChanged, map} from 'rxjs/operators';
+import {Finalizable} from '../registry';
+import {NumericChangeId, PatchSetNum} from '../../types/common';
+import {Model} from '../../models/model';
export enum GerritView {
ADMIN = 'admin',
@@ -33,6 +35,7 @@
ROOT = 'root',
SEARCH = 'search',
SETTINGS = 'settings',
+ TOPIC = 'topic',
}
export interface RouterState {
@@ -41,41 +44,40 @@
patchNum?: PatchSetNum;
}
-// TODO: Figure out how to best enforce immutability of all states. Use Immer?
-// Use DeepReadOnly?
-const initialState: RouterState = {};
+export class RouterModel extends Model<RouterState> implements Finalizable {
+ readonly routerView$: Observable<GerritView | undefined>;
-const privateState$ = new BehaviorSubject<RouterState>(initialState);
+ readonly routerChangeNum$: Observable<NumericChangeId | undefined>;
-// Re-exporting as Observable so that you can only subscribe, but not emit.
-export const routerState$: Observable<RouterState> = privateState$;
+ readonly routerPatchNum$: Observable<PatchSetNum | undefined>;
-// Must only be used by the router service or whatever is in control of this
-// model.
-export function updateState(
- view?: GerritView,
- changeNum?: NumericChangeId,
- patchNum?: PatchSetNum
-) {
- privateState$.next({
- ...privateState$.getValue(),
- view,
- changeNum,
- patchNum,
- });
+ constructor() {
+ super({});
+ this.routerView$ = this.state$.pipe(
+ map(state => state.view),
+ distinctUntilChanged()
+ );
+ this.routerChangeNum$ = this.state$.pipe(
+ map(state => state.changeNum),
+ distinctUntilChanged()
+ );
+ this.routerPatchNum$ = this.state$.pipe(
+ map(state => state.patchNum),
+ distinctUntilChanged()
+ );
+ }
+
+ finalize() {}
+
+ // Private but used in tests
+ setState(state: RouterState) {
+ this.subject$.next(state);
+ }
+
+ updateState(partial: Partial<RouterState>) {
+ this.subject$.next({
+ ...this.subject$.getValue(),
+ ...partial,
+ });
+ }
}
-
-export const routerView$ = routerState$.pipe(
- map(state => state.view),
- distinctUntilChanged()
-);
-
-export const routerChangeNum$ = routerState$.pipe(
- map(state => state.changeNum),
- distinctUntilChanged()
-);
-
-export const routerPatchNum$ = routerState$.pipe(
- map(state => state.patchNum),
- distinctUntilChanged()
-);
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts
index 3c9e058..ed68f15 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts
@@ -52,7 +52,6 @@
OPEN_CHANGE = 'OPEN_CHANGE',
NEXT_PAGE = 'NEXT_PAGE',
PREV_PAGE = 'PREV_PAGE',
- TOGGLE_CHANGE_REVIEWED = 'TOGGLE_CHANGE_REVIEWED',
TOGGLE_CHANGE_STAR = 'TOGGLE_CHANGE_STAR',
REFRESH_CHANGE_LIST = 'REFRESH_CHANGE_LIST',
OPEN_SUBMIT_DIALOG = 'OPEN_SUBMIT_DIALOG',
@@ -180,13 +179,13 @@
Shortcut.CURSOR_NEXT_CHANGE,
ShortcutSection.ACTIONS,
'Select next change',
- {key: 'j'}
+ {key: 'j', allowRepeat: true}
);
describe(
Shortcut.CURSOR_PREV_CHANGE,
ShortcutSection.ACTIONS,
'Select previous change',
- {key: 'k'}
+ {key: 'k', allowRepeat: true}
);
describe(
Shortcut.OPEN_CHANGE,
@@ -239,12 +238,6 @@
{key: 'R'}
);
describe(
- Shortcut.TOGGLE_CHANGE_REVIEWED,
- ShortcutSection.ACTIONS,
- 'Mark/unmark change as reviewed',
- {key: 'r'}
-);
-describe(
Shortcut.TOGGLE_FILE_REVIEWED,
ShortcutSection.ACTIONS,
'Toggle review flag on selected file',
@@ -316,15 +309,15 @@
Shortcut.NEXT_LINE,
ShortcutSection.DIFFS,
'Go to next line',
- {key: 'j'},
- {key: Key.DOWN}
+ {key: 'j', allowRepeat: true},
+ {key: Key.DOWN, allowRepeat: true}
);
describe(
Shortcut.PREV_LINE,
ShortcutSection.DIFFS,
'Go to previous line',
- {key: 'k'},
- {key: Key.UP}
+ {key: 'k', allowRepeat: true},
+ {key: Key.UP, allowRepeat: true}
);
describe(
Shortcut.VISIBLE_LINE,
@@ -480,15 +473,15 @@
Shortcut.CURSOR_NEXT_FILE,
ShortcutSection.FILE_LIST,
'Select next file',
- {key: 'j'},
- {key: Key.DOWN}
+ {key: 'j', allowRepeat: true},
+ {key: Key.DOWN, allowRepeat: true}
);
describe(
Shortcut.CURSOR_PREV_FILE,
ShortcutSection.FILE_LIST,
'Select previous file',
- {key: 'k'},
- {key: Key.UP}
+ {key: 'k', allowRepeat: true},
+ {key: Key.UP, allowRepeat: true}
);
describe(
Shortcut.OPEN_FILE,
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
index a26fa08..41591a2 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
@@ -14,13 +14,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {Subscription} from 'rxjs';
+import {map, distinctUntilChanged} from 'rxjs/operators';
import {
config,
Shortcut,
ShortcutHelpItem,
ShortcutSection,
} from './shortcuts-config';
-import {disableShortcuts$} from '../user/user-model';
import {
ComboKey,
eventMatchesShortcut,
@@ -31,6 +32,8 @@
shouldSuppress,
} from '../../utils/dom-util';
import {ReportingService} from '../gr-reporting/gr-reporting';
+import {Finalizable} from '../registry';
+import {UserModel} from '../../models/user/user-model';
export type SectionView = Array<{binding: string[][]; text: string}>;
@@ -62,13 +65,13 @@
/**
* Shortcuts service, holds all hosts, bindings and listeners.
*/
-export class ShortcutsService {
+export class ShortcutsService implements Finalizable {
/**
* Keeps track of the components that are currently active such that we can
* show a shortcut help dialog that only shows the shortcuts that are
* currently relevant.
*/
- private readonly activeShortcuts = new Map<HTMLElement, Shortcut[]>();
+ private readonly activeShortcuts = new Set<Shortcut>();
/**
* Keeps track of cleanup callbacks (which remove keyboard listeners) that
@@ -92,19 +95,41 @@
/** Keeps track of the corresponding user preference. */
private shortcutsDisabled = false;
- constructor(readonly reporting?: ReportingService) {
+ private readonly keydownListener: (e: KeyboardEvent) => void;
+
+ private readonly subscriptions: Subscription[] = [];
+
+ constructor(
+ readonly userModel: UserModel,
+ readonly reporting?: ReportingService
+ ) {
for (const section of config.keys()) {
const items = config.get(section) ?? [];
for (const item of items) {
this.bindings.set(item.shortcut, item.bindings);
}
}
- disableShortcuts$.subscribe(x => (this.shortcutsDisabled = x));
- document.addEventListener('keydown', (e: KeyboardEvent) => {
+ this.subscriptions.push(
+ this.userModel.preferences$
+ .pipe(
+ map(preferences => preferences?.disable_keyboard_shortcuts ?? false),
+ distinctUntilChanged()
+ )
+ .subscribe(x => (this.shortcutsDisabled = x))
+ );
+ this.keydownListener = (e: KeyboardEvent) => {
if (!isComboKey(e.key)) return;
- if (this.shouldSuppress(e)) return;
+ if (this.shortcutsDisabled || shouldSuppress(e)) return;
this.comboKeyLastPressed = {key: e.key, timestampMs: Date.now()};
- });
+ };
+ document.addEventListener('keydown', this.keydownListener);
+ }
+
+ finalize() {
+ document.removeEventListener('keydown', this.keydownListener);
+ for (const s of this.subscriptions) {
+ s.unsubscribe();
+ }
}
public _testOnly_isEmpty() {
@@ -134,29 +159,36 @@
addShortcut(
element: HTMLElement,
shortcut: Binding,
- listener: (e: KeyboardEvent) => void
+ listener: (e: KeyboardEvent) => void,
+ options: {
+ shouldSuppress: boolean;
+ } = {
+ shouldSuppress: true,
+ }
) {
const wrappedListener = (e: KeyboardEvent) => {
- if (e.repeat) return;
+ if (e.repeat && !shortcut.allowRepeat) return;
if (!eventMatchesShortcut(e, shortcut)) return;
if (shortcut.combo) {
if (!this.isInSpecificComboKeyMode(shortcut.combo)) return;
} else {
if (this.isInComboKeyMode()) return;
}
- if (this.shouldSuppress(e)) return;
+ if (options.shouldSuppress && shouldSuppress(e)) return;
+ // `shortcutsDisabled` refers to disabling global shortcuts like 'n'. If
+ // `shouldSuppress` is false (e.g.for Ctrl - ENTER), then don't disable
+ // the shortcut.
+ if (options.shouldSuppress && this.shortcutsDisabled) return;
e.preventDefault();
e.stopPropagation();
+ this.reportTriggered(e);
listener(e);
};
element.addEventListener('keydown', wrappedListener);
return () => element.removeEventListener('keydown', wrappedListener);
}
- shouldSuppress(e: KeyboardEvent) {
- if (this.shortcutsDisabled) return true;
- if (shouldSuppress(e)) return true;
-
+ private reportTriggered(e: KeyboardEvent) {
// eg: {key: "k:keydown", ..., from: "gr-diff-view"}
let key = `${e.key}:${e.type}`;
if (this.isInSpecificComboKeyMode(ComboKey.G)) key = 'g+' + key;
@@ -170,7 +202,6 @@
from = e.currentTarget.tagName;
}
this.reporting?.reportInteraction('shortcut-triggered', {key, from});
- return false;
}
createTitle(shortcutName: Shortcut, section: ShortcutSection) {
@@ -183,28 +214,47 @@
return this.bindings.get(shortcut);
}
+ /**
+ * Looks up bindings for the given shortcut and calls addShortcut() for each
+ * of them. Also adds the shortcut to `activeShortcuts` and thus to the
+ * help page about active shortcuts. Returns a cleanup function for removing
+ * the bindings and the help page entry.
+ */
+ addShortcutListener(
+ shortcut: Shortcut,
+ listener: (e: KeyboardEvent) => void
+ ) {
+ const cleanups: (() => void)[] = [];
+ this.activeShortcuts.add(shortcut);
+ cleanups.push(() => {
+ this.activeShortcuts.delete(shortcut);
+ this.notifyViewListeners();
+ });
+ const bindings = this.getBindingsForShortcut(shortcut);
+ for (const binding of bindings ?? []) {
+ if (binding.docOnly) continue;
+ cleanups.push(this.addShortcut(document.body, binding, listener));
+ }
+ this.notifyViewListeners();
+ return () => {
+ for (const cleanup of cleanups ?? []) cleanup();
+ };
+ }
+
+ /**
+ * Being called by the Polymer specific KeyboardShortcutMixin.
+ */
attachHost(host: HTMLElement, shortcuts: ShortcutListener[]) {
- this.activeShortcuts.set(
- host,
- shortcuts.map(s => s.shortcut)
- );
const cleanups: (() => void)[] = [];
for (const s of shortcuts) {
- const bindings = this.getBindingsForShortcut(s.shortcut);
- for (const binding of bindings ?? []) {
- if (binding.docOnly) continue;
- cleanups.push(this.addShortcut(document.body, binding, s.listener));
- }
+ cleanups.push(this.addShortcutListener(s.shortcut, s.listener));
}
this.cleanupsPerHost.set(host, cleanups);
- this.notifyViewListeners();
}
detachHost(host: HTMLElement) {
- this.activeShortcuts.delete(host);
const cleanups = this.cleanupsPerHost.get(host);
for (const cleanup of cleanups ?? []) cleanup();
- this.notifyViewListeners();
return true;
}
@@ -233,20 +283,13 @@
}
activeShortcutsBySection() {
- const activeShortcuts = new Set<Shortcut>();
- for (const shortcuts of this.activeShortcuts.values()) {
- for (const shortcut of shortcuts) {
- activeShortcuts.add(shortcut);
- }
- }
-
const activeShortcutsBySection = new Map<
ShortcutSection,
ShortcutHelpItem[]
>();
config.forEach((shortcutList, section) => {
shortcutList.forEach(shortcutHelp => {
- if (activeShortcuts.has(shortcutHelp.shortcut)) {
+ if (this.activeShortcuts.has(shortcutHelp.shortcut)) {
if (!activeShortcutsBySection.has(section)) {
activeShortcutsBySection.set(section, []);
}
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
index 05c4f53..7dd3f75 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service_test.ts
@@ -21,80 +21,18 @@
ShortcutsService,
} from '../../services/shortcuts/shortcuts-service';
import {Shortcut, ShortcutSection} from './shortcuts-config';
-import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import {SinonFakeTimers} from 'sinon';
import {Key, Modifier} from '../../utils/dom-util';
-
-async function keyEventOn(
- el: HTMLElement,
- callback: (e: KeyboardEvent) => void,
- keyCode = 75,
- key = 'k'
-): Promise<KeyboardEvent> {
- let resolve: (e: KeyboardEvent) => void;
- const promise = new Promise<KeyboardEvent>(r => (resolve = r));
- el.addEventListener('keydown', (e: KeyboardEvent) => {
- callback(e);
- resolve(e);
- });
- MockInteractions.keyDownOn(el, keyCode, null, key);
- return await promise;
-}
+import {getAppContext} from '../app-context';
suite('shortcuts-service tests', () => {
let service: ShortcutsService;
setup(() => {
- service = new ShortcutsService();
- });
-
- suite('shouldSuppress', () => {
- test('do not suppress shortcut event from <div>', async () => {
- await keyEventOn(document.createElement('div'), e => {
- assert.isFalse(service.shouldSuppress(e));
- });
- });
-
- test('suppress shortcut event from <input>', async () => {
- await keyEventOn(document.createElement('input'), e => {
- assert.isTrue(service.shouldSuppress(e));
- });
- });
-
- test('suppress shortcut event from <textarea>', async () => {
- await keyEventOn(document.createElement('textarea'), e => {
- assert.isTrue(service.shouldSuppress(e));
- });
- });
-
- test('do not suppress shortcut event from checkbox <input>', async () => {
- const inputEl = document.createElement('input');
- inputEl.setAttribute('type', 'checkbox');
- await keyEventOn(inputEl, e => {
- assert.isFalse(service.shouldSuppress(e));
- });
- });
-
- test('suppress shortcut event from children of <gr-overlay>', async () => {
- const overlay = document.createElement('gr-overlay');
- const div = document.createElement('div');
- overlay.appendChild(div);
- await keyEventOn(div, e => {
- assert.isTrue(service.shouldSuppress(e));
- });
- });
-
- test('suppress "enter" shortcut event from <a>', async () => {
- await keyEventOn(document.createElement('a'), e => {
- assert.isFalse(service.shouldSuppress(e));
- });
- await keyEventOn(
- document.createElement('a'),
- e => assert.isTrue(service.shouldSuppress(e)),
- 13,
- 'enter'
- );
- });
+ service = new ShortcutsService(
+ getAppContext().userModel,
+ getAppContext().reportingService
+ );
});
test('getShortcut', () => {
@@ -213,7 +151,10 @@
{
shortcut: Shortcut.NEXT_LINE,
text: 'Go to next line',
- bindings: [{key: 'j'}, {key: 'ArrowDown'}],
+ bindings: [
+ {allowRepeat: true, key: 'j'},
+ {allowRepeat: true, key: 'ArrowDown'},
+ ],
},
],
[ShortcutSection.NAVIGATION]: [
@@ -234,7 +175,10 @@
{
shortcut: Shortcut.NEXT_LINE,
text: 'Go to next line',
- bindings: [{key: 'j'}, {key: 'ArrowDown'}],
+ bindings: [
+ {allowRepeat: true, key: 'j'},
+ {allowRepeat: true, key: 'ArrowDown'},
+ ],
},
],
[ShortcutSection.EVERYWHERE]: [
diff --git a/polygerrit-ui/app/services/storage/gr-storage.ts b/polygerrit-ui/app/services/storage/gr-storage.ts
index 08a3387..0b995d8 100644
--- a/polygerrit-ui/app/services/storage/gr-storage.ts
+++ b/polygerrit-ui/app/services/storage/gr-storage.ts
@@ -16,6 +16,7 @@
*/
import {CommentRange, PatchSetNum} from '../../types/common';
+import {Finalizable} from '../registry';
export interface StorageLocation {
changeNum: number;
@@ -30,7 +31,7 @@
updated: number;
}
-export interface StorageService {
+export interface StorageService extends Finalizable {
getDraftComment(location: StorageLocation): StorageObject | null;
setDraftComment(location: StorageLocation, message: string): void;
diff --git a/polygerrit-ui/app/services/storage/gr-storage_impl.ts b/polygerrit-ui/app/services/storage/gr-storage_impl.ts
index 0c0d151..5c8a765 100644
--- a/polygerrit-ui/app/services/storage/gr-storage_impl.ts
+++ b/polygerrit-ui/app/services/storage/gr-storage_impl.ts
@@ -16,6 +16,7 @@
*/
import {StorageLocation, StorageObject, StorageService} from './gr-storage';
+import {Finalizable} from '../registry';
export const DURATION_DAY = 24 * 60 * 60 * 1000;
@@ -27,13 +28,15 @@
CLEANUP_PREFIXES_MAX_AGE_MAP.set('draft', DURATION_DAY);
CLEANUP_PREFIXES_MAX_AGE_MAP.set('editablecontent', DURATION_DAY);
-export class GrStorageService implements StorageService {
+export class GrStorageService implements StorageService, Finalizable {
private lastCleanup = 0;
private readonly storage = window.localStorage;
private exceededQuota = false;
+ finalize() {}
+
getDraftComment(location: StorageLocation): StorageObject | null {
this.cleanupItems();
return this.getObject(this.getDraftKey(location));
@@ -136,16 +139,17 @@
}
try {
this.storage.setItem(key, JSON.stringify(obj));
- } catch (exc) {
- // Catch for QuotaExceededError and disable writes on local storage the
- // first time that it occurs.
- if (exc.code === 22) {
- this.exceededQuota = true;
- console.warn('Local storage quota exceeded: disabling');
- return;
- } else {
- throw exc;
+ } catch (exc: unknown) {
+ if (exc instanceof DOMException) {
+ // Catch for QuotaExceededError and disable writes on local storage the
+ // first time that it occurs.
+ if (exc.code === 22) {
+ this.exceededQuota = true;
+ console.warn('Local storage quota exceeded: disabling');
+ return;
+ }
}
+ throw exc;
}
}
}
diff --git a/polygerrit-ui/app/services/storage/gr-storage_mock.ts b/polygerrit-ui/app/services/storage/gr-storage_mock.ts
index 399ffe4..79f0fbc 100644
--- a/polygerrit-ui/app/services/storage/gr-storage_mock.ts
+++ b/polygerrit-ui/app/services/storage/gr-storage_mock.ts
@@ -45,6 +45,7 @@
}
export const grStorageMock: StorageService = {
+ finalize(): void {},
getDraftComment(location: StorageLocation): StorageObject | null {
return storage.get(getDraftKey(location)) ?? null;
},
diff --git a/polygerrit-ui/app/services/storage/gr-storage_test.js b/polygerrit-ui/app/services/storage/gr-storage_test.js
index 6cbfacf..93567ec 100644
--- a/polygerrit-ui/app/services/storage/gr-storage_test.js
+++ b/polygerrit-ui/app/services/storage/gr-storage_test.js
@@ -26,8 +26,9 @@
getItem(key) { return this[key]; },
removeItem(key) { delete this[key]; },
setItem(key, value) {
- // eslint-disable-next-line no-throw-literal
- if (opt_quotaExceeded) { throw {code: 22}; /* Quota exceeded */ }
+ if (opt_quotaExceeded) {
+ throw new DOMException('error', 'QuotaExceededError');
+ }
this[key] = value;
},
};
diff --git a/polygerrit-ui/app/services/user/user-model.ts b/polygerrit-ui/app/services/user/user-model.ts
deleted file mode 100644
index 72ce3e1..0000000
--- a/polygerrit-ui/app/services/user/user-model.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * @license
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {AccountDetailInfo, PreferencesInfo} from '../../types/common';
-import {BehaviorSubject, Observable} from 'rxjs';
-import {map, distinctUntilChanged} from 'rxjs/operators';
-import {createDefaultPreferences} from '../../constants/constants';
-
-interface UserState {
- /**
- * Keeps being defined even when credentials have expired.
- */
- account?: AccountDetailInfo;
- preferences: PreferencesInfo;
-}
-
-const initialState: UserState = {
- preferences: createDefaultPreferences(),
-};
-
-const privateState$ = new BehaviorSubject(initialState);
-
-// Re-exporting as Observable so that you can only subscribe, but not emit.
-export const userState$: Observable<UserState> = privateState$;
-
-export function updateAccount(account?: AccountDetailInfo) {
- const current = privateState$.getValue();
- privateState$.next({...current, account});
-}
-
-export function updatePreferences(preferences: PreferencesInfo) {
- const current = privateState$.getValue();
- privateState$.next({...current, preferences});
-}
-
-export const account$ = userState$.pipe(
- map(userState => userState.account),
- distinctUntilChanged()
-);
-
-export const preferences$ = userState$.pipe(
- map(userState => userState.preferences),
- distinctUntilChanged()
-);
-
-export const myTopMenuItems$ = preferences$.pipe(
- map(preferences => preferences?.my ?? []),
- distinctUntilChanged()
-);
-
-export const disableShortcuts$ = preferences$.pipe(
- map(preferences => preferences?.disable_keyboard_shortcuts ?? false),
- distinctUntilChanged()
-);
diff --git a/polygerrit-ui/app/services/user/user-service.ts b/polygerrit-ui/app/services/user/user-service.ts
deleted file mode 100644
index 125d20c..0000000
--- a/polygerrit-ui/app/services/user/user-service.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * @license
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {
- AccountDetailInfo,
- PreferencesInfo,
- PreferencesInput,
-} from '../../types/common';
-import {from, of} from 'rxjs';
-import {account$, updateAccount, updatePreferences} from './user-model';
-import {switchMap} from 'rxjs/operators';
-import {createDefaultPreferences} from '../../constants/constants';
-import {RestApiService} from '../gr-rest-api/gr-rest-api';
-
-export class UserService {
- constructor(readonly restApiService: RestApiService) {
- from(this.restApiService.getAccount()).subscribe(
- (account?: AccountDetailInfo) => {
- updateAccount(account);
- }
- );
- account$
- .pipe(
- switchMap(account => {
- if (!account) return of(createDefaultPreferences());
- return from(this.restApiService.getPreferences());
- })
- )
- .subscribe((preferences?: PreferencesInfo) => {
- updatePreferences(preferences ?? createDefaultPreferences());
- });
- }
-
- updatePreferences(prefs: PreferencesInput) {
- this.restApiService
- .savePreferences(prefs)
- .then((newPrefs: PreferencesInfo | undefined) => {
- if (!newPrefs) return;
- updatePreferences(newPrefs);
- });
- }
-}
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.ts b/polygerrit-ui/app/styles/gr-change-list-styles.ts
index e0a7a28..e0ce1d7 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.ts
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.ts
@@ -14,191 +14,186 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {css} from 'lit';
-// Mark the file as a module. Otherwise typescript assumes this is a script
-// and $_documentContainer is a global variable.
-// See: https://www.typescriptlang.org/docs/handbook/modules.html
-export {};
+export const changeListStyles = css`
+ gr-change-list-item {
+ border-top: 1px solid var(--border-color);
+ }
+ gr-change-list-item[selected],
+ gr-change-list-item:focus {
+ background-color: var(--selection-background-color);
+ }
+ gr-change-list-item[highlight] {
+ background-color: var(--assignee-highlight-color);
+ }
+ gr-change-list-item[highlight][selected],
+ gr-change-list-item[highlight]:focus {
+ background-color: var(--assignee-highlight-selection-color);
+ }
+ .groupTitle td,
+ .cell {
+ vertical-align: middle;
+ }
+ .groupTitle td:not(.label):not(.endpoint),
+ .cell:not(.label):not(.endpoint) {
+ padding-right: 8px;
+ }
+ .groupTitle td {
+ color: var(--deemphasized-text-color);
+ text-align: left;
+ }
+ .groupHeader {
+ background-color: transparent;
+ font-size: var(--font-size-h3);
+ font-weight: var(--font-weight-h3);
+ line-height: var(--line-height-h3);
+ }
+ .groupContent {
+ background-color: var(--background-color-primary);
+ box-shadow: var(--elevation-level-1);
+ }
+ .groupHeader a {
+ color: var(--primary-text-color);
+ text-decoration: none;
+ }
+ .groupHeader a:hover {
+ text-decoration: underline;
+ }
+ .groupTitle td,
+ .cell {
+ padding: var(--spacing-s) 0;
+ }
+ .groupHeader .cell {
+ padding-top: var(--spacing-l);
+ }
+ .star {
+ padding: 0;
+ }
+ gr-change-star {
+ vertical-align: middle;
+ }
+ .owner {
+ --account-max-length: 100px;
+ }
+ .branch,
+ .star,
+ .label,
+ .number,
+ .owner,
+ .updated,
+ .submitted,
+ .waiting,
+ .size,
+ .status,
+ .repo {
+ white-space: nowrap;
+ }
+ .star {
+ vertical-align: middle;
+ }
+ .leftPadding {
+ width: var(--spacing-l);
+ }
+ .star {
+ width: 30px;
+ }
+ .reviewers div {
+ overflow: hidden;
+ }
+ .label,
+ .endpoint {
+ border-left: 1px solid var(--border-color);
+ }
+ .groupTitle td.label,
+ .label {
+ text-align: center;
+ width: 3rem;
+ }
+ .truncatedRepo {
+ display: none;
+ }
+ @media only screen and (max-width: 150em) {
+ .branch {
+ overflow: hidden;
+ max-width: 18rem;
+ text-overflow: ellipsis;
+ }
+ .truncatedRepo {
+ display: inline-block;
+ }
+ .fullRepo {
+ display: none;
+ }
+ }
+ @media only screen and (max-width: 100em) {
+ .branch {
+ max-width: 10rem;
+ }
+ }
+ @media only screen and (max-width: 50em) {
+ :host {
+ font-family: var(--header-font-family);
+ font-size: var(--font-size-h3);
+ font-weight: var(--font-weight-h3);
+ line-height: var(--line-height-h3);
+ }
+ gr-change-list-item {
+ flex-wrap: wrap;
+ justify-content: space-between;
+ padding: var(--spacing-xs) var(--spacing-m);
+ }
+ gr-change-list-item[selected],
+ gr-change-list-item:focus {
+ background-color: var(--view-background-color);
+ border: none;
+ border-top: 1px solid var(--border-color);
+ }
+ gr-change-list-item:hover {
+ background-color: var(--view-background-color);
+ }
+ .cell {
+ align-items: center;
+ display: flex;
+ }
+ .groupTitle,
+ .leftPadding,
+ .status,
+ .repo,
+ .branch,
+ .updated,
+ .submitted,
+ .waiting,
+ .label,
+ .groupHeader .star,
+ .noChanges .star {
+ display: none;
+ }
+ .groupHeader .cell,
+ .noChanges .cell {
+ padding-left: var(--spacing-m);
+ }
+ .subject {
+ margin-bottom: var(--spacing-xs);
+ width: calc(100% - 2em);
+ }
+ .owner,
+ .size {
+ max-width: none;
+ }
+ .noChanges .cell {
+ display: block;
+ height: auto;
+ }
+ }
+`;
const $_documentContainer = document.createElement('template');
-
$_documentContainer.innerHTML = `<dom-module id="gr-change-list-styles">
<template>
<style>
- gr-change-list-item {
- border-top: 1px solid var(--border-color);
- }
- gr-change-list-item[selected],
- gr-change-list-item:focus {
- background-color: var(--selection-background-color);
- }
- gr-change-list-item[highlight] {
- background-color: var(--assignee-highlight-color);
- }
- gr-change-list-item[highlight][selected],
- gr-change-list-item[highlight]:focus {
- background-color: var(--assignee-highlight-selection-color);
- }
- .groupTitle td,
- .cell {
- vertical-align: middle;
- }
- .groupTitle td:not(.label):not(.endpoint),
- .cell:not(.label):not(.endpoint) {
- padding-right: 8px;
- }
- .groupTitle td {
- color: var(--deemphasized-text-color);
- text-align: left;
- }
- .groupHeader {
- background-color: transparent;
- font-size: var(--font-size-h3);
- font-weight: var(--font-weight-h3);
- line-height: var(--line-height-h3);
- }
- .groupContent {
- background-color: var(--background-color-primary);
- box-shadow: var(--elevation-level-1);
- }
- .groupHeader a {
- color: var(--primary-text-color);
- text-decoration: none;
- }
- .groupHeader a:hover {
- text-decoration: underline;
- }
- .groupTitle td,
- .cell {
- padding: var(--spacing-s) 0;
- }
- .groupHeader .cell {
- padding-top: var(--spacing-l);
- }
- .star {
- padding: 0;
- }
- gr-change-star {
- vertical-align: middle;
- }
- .owner {
- --account-max-length: 100px;
- }
- .branch,
- .star,
- .label,
- .number,
- .owner,
- .assignee,
- .updated,
- .submitted,
- .waiting,
- .size,
- .status,
- .repo {
- white-space: nowrap;
- }
- .star {
- vertical-align: middle;
- }
- .leftPadding {
- width: var(--spacing-l);
- }
- .star {
- width: 30px;
- }
- .reviewers div {
- overflow: hidden;
- }
- .label, .endpoint {
- border-left: 1px solid var(--border-color);
- }
- .groupTitle td.label,
- .label {
- text-align: center;
- width: 3rem;
- }
- .truncatedRepo {
- display: none;
- }
- @media only screen and (max-width: 150em) {
- .assignee,
- .branch {
- overflow: hidden;
- max-width: 18rem;
- text-overflow: ellipsis;
- }
- .truncatedRepo {
- display: inline-block;
- }
- .fullRepo {
- display: none;
- }
- }
- @media only screen and (max-width: 100em) {
- .assignee,
- .branch {
- max-width: 10rem;
- }
- }
- @media only screen and (max-width: 50em) {
- :host {
- font-family: var(--header-font-family);
- font-size: var(--font-size-h3);
- font-weight: var(--font-weight-h3);
- line-height: var(--line-height-h3);
- }
- gr-change-list-item {
- flex-wrap: wrap;
- justify-content: space-between;
- padding: var(--spacing-xs) var(--spacing-m);
- }
- gr-change-list-item[selected],
- gr-change-list-item:focus {
- background-color: var(--view-background-color);
- border: none;
- border-top: 1px solid var(--border-color);
- }
- gr-change-list-item:hover {
- background-color: var(--view-background-color);
- }
- .cell {
- align-items: center;
- display: flex;
- }
- .groupTitle,
- .leftPadding,
- .status,
- .repo,
- .branch,
- .updated,
- .submitted,
- .waiting,
- .label,
- .assignee,
- .groupHeader .star,
- .noChanges .star {
- display: none;
- }
- .groupHeader .cell,
- .noChanges .cell {
- padding-left: var(--spacing-m);
- }
- .subject {
- margin-bottom: var(--spacing-xs);
- width: calc(100% - 2em);
- }
- .owner,
- .size {
- max-width: none;
- }
- .noChanges .cell {
- display: block;
- height: auto;
- }
- }
+ ${changeListStyles.cssText}
</style>
</template>
</dom-module>`;
-
document.head.appendChild($_documentContainer.content);
diff --git a/polygerrit-ui/app/styles/gr-paper-styles.ts b/polygerrit-ui/app/styles/gr-paper-styles.ts
new file mode 100644
index 0000000..1ef7124
--- /dev/null
+++ b/polygerrit-ui/app/styles/gr-paper-styles.ts
@@ -0,0 +1,60 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {css} from 'lit';
+
+export const paperStyles = css`
+ paper-toggle-button {
+ --paper-toggle-button-checked-bar-color: var(--link-color);
+ --paper-toggle-button-checked-button-color: var(--link-color);
+ }
+ paper-tabs {
+ font-size: var(--font-size-h3);
+ font-weight: var(--font-weight-h3);
+ line-height: var(--line-height-h3);
+ --paper-font-common-base: {
+ font-family: var(--header-font-family);
+ -webkit-font-smoothing: initial;
+ }
+ --paper-tab-content: {
+ margin-bottom: var(--spacing-s);
+ }
+ --paper-tab-content-focused: {
+ /* paper-tabs uses 700 here, which can look awkward */
+ font-weight: var(--font-weight-h3);
+ background: var(--gray-background-focus);
+ }
+ --paper-tab-content-unselected: {
+ /* paper-tabs uses 0.8 here, but we want to control the color directly */
+ opacity: 1;
+ color: var(--deemphasized-text-color);
+ }
+ }
+ paper-tab:focus {
+ padding-left: 0px;
+ padding-right: 0px;
+ }
+`;
+
+const $_documentContainer = document.createElement('template');
+$_documentContainer.innerHTML = `<dom-module id="gr-paper-styles">
+ <template>
+ <style>
+ ${paperStyles.cssText}
+ </style>
+ </template>
+</dom-module>`;
+document.head.appendChild($_documentContainer.content);
diff --git a/polygerrit-ui/app/elements/gr-app_html.ts b/polygerrit-ui/app/styles/gr-submit-requirements-styles.ts
similarity index 64%
rename from polygerrit-ui/app/elements/gr-app_html.ts
rename to polygerrit-ui/app/styles/gr-submit-requirements-styles.ts
index f6172c9..8c5deef 100644
--- a/polygerrit-ui/app/elements/gr-app_html.ts
+++ b/polygerrit-ui/app/styles/gr-submit-requirements-styles.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {html} from '@polymer/polymer/lib/utils/html-tag';
+import {css} from 'lit';
-export const htmlTemplate = html`
- <gr-app-element id="app-element"></gr-app-element>
+export const submitRequirementsStyles = css`
+ iron-icon.check-circle-filled,
+ iron-icon.overridden {
+ color: var(--success-foreground);
+ }
+ iron-icon.block,
+ iron-icon.error {
+ color: var(--deemphasized-text-color);
+ }
`;
diff --git a/polygerrit-ui/app/styles/shared-styles.ts b/polygerrit-ui/app/styles/shared-styles.ts
index 98f6eb2..e99cf27 100644
--- a/polygerrit-ui/app/styles/shared-styles.ts
+++ b/polygerrit-ui/app/styles/shared-styles.ts
@@ -189,36 +189,6 @@
.separator.transparent {
border-color: transparent;
}
- paper-toggle-button {
- --paper-toggle-button-checked-bar-color: var(--link-color);
- --paper-toggle-button-checked-button-color: var(--link-color);
- }
- paper-tabs {
- font-size: var(--font-size-h3);
- font-weight: var(--font-weight-h3);
- line-height: var(--line-height-h3);
- --paper-font-common-base: {
- font-family: var(--header-font-family);
- -webkit-font-smoothing: initial;
- }
- --paper-tab-content: {
- margin-bottom: var(--spacing-s);
- }
- --paper-tab-content-focused: {
- /* paper-tabs uses 700 here, which can look awkward */
- font-weight: var(--font-weight-h3);
- background: var(--gray-background-focus);
- }
- --paper-tab-content-unselected: {
- /* paper-tabs uses 0.8 here, but we want to control the color directly */
- opacity: 1;
- color: var(--deemphasized-text-color);
- }
- }
- paper-tab:focus {
- padding-left: 0px;
- padding-right: 0px;
- }
iron-autogrow-textarea {
/** This is needed for firefox */
--iron-autogrow-textarea_-_white-space: pre-wrap;
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index 8b00a79..695809e 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -100,6 +100,7 @@
--gray-700-10: #5f63681a;
--gray-700-12: #5f63681f;
--gray-500: #9aa0a6;
+ --gray-400: #bdc1c6;
--gray-300: #dadce0;
--gray-200: #e8eaed;
--gray-200-12: #e8eaed1f;
@@ -220,6 +221,16 @@
--selection-background-color: rgba(161, 194, 250, 0.1);
--tooltip-background-color: var(--gray-900);
+ /* dashboard size background colors */
+ --dashboard-size-xs: var(--gray-200);
+ --dashboard-size-s: var(--gray-300);
+ --dashboard-size-m: var(--gray-400);
+ --dashboard-size-l: var(--gray-500);
+ --dashboard-size-xl: var(--gray-700);
+ --dashboard-size-text: black;
+ --dashboard-size-xs-text: black;
+ --dashboard-size-xl-text: white;
+
/* comment background colors */
--comment-background-color: var(--gray-200);
--robot-comment-background-color: var(--blue-50);
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index 05c6b7d..7d28c8a 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -175,8 +175,18 @@
--header-padding: 0 var(--spacing-l);
--header-text-color: var(--primary-text-color);
+ /* dashboard size background colors */
+ --dashboard-size-xs: var(--gray-700);
+ --dashboard-size-s: var(--gray-500);
+ --dashboard-size-m: var(--gray-400);
+ --dashboard-size-l: var(--gray-300);
+ --dashboard-size-xl: var(--gray-200);
+ --dashboard-size-text: black;
+ --dashboard-size-xs-text: white;
+ --dashboard-size-xl-text: black;
+
/* diff colors */
- --dark-add-highlight-color: var(--green-tonal);
+ --dark-add-highlight-color: var(--green-tonal);
--dark-rebased-add-highlight-color: rgba(11, 255, 155, 0.15);
--dark-rebased-remove-highlight-color: rgba(255, 139, 6, 0.15);
--dark-remove-highlight-color: #62110f;
diff --git a/polygerrit-ui/app/test/common-test-setup-karma.ts b/polygerrit-ui/app/test/common-test-setup-karma.ts
index 39c79d1..888ace0 100644
--- a/polygerrit-ui/app/test/common-test-setup-karma.ts
+++ b/polygerrit-ui/app/test/common-test-setup-karma.ts
@@ -36,6 +36,7 @@
// For uncaught error mochajs doesn't print the full stack trace.
// We should print it ourselves.
console.error('Uncaught error:');
+ console.error(e);
console.error(e.error.stack.toString());
unhandledError = e;
});
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index bd5504a..23c4141 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -21,9 +21,15 @@
import '../scripts/bundled-polymer';
import '@polymer/iron-test-helpers/iron-test-helpers';
import './test-router';
-import {_testOnlyInitAppContext} from './test-app-context-init';
+import {AppContext, injectAppContext} 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 '../elements/shared/gr-rest-api-interface/gr-rest-api-interface';
+import {_testOnlyResetGrRestApiSharedObjects} from '../elements/shared/gr-rest-api-interface/gr-rest-api-impl';
import {
cleanupTestUtils,
getCleanupsCount,
@@ -33,18 +39,21 @@
removeThemeStyles,
} from './test-utils';
import {safeTypesBridge} from '../utils/safe-types-util';
-import {_testOnly_initGerritPluginApi} from '../elements/shared/gr-js-api-interface/gr-gerrit';
import {initGlobalVariables} from '../elements/gr-app-global-var-init';
import 'chai/chai';
+import {chaiDomDiff} from '@open-wc/semantic-dom-diff';
import {
_testOnly_defaultResinReportHandler,
installPolymerResin,
} from '../scripts/polymer-resin-install';
import {_testOnly_allTasks} from '../utils/async-util';
import {cleanUpStorage} from '../services/storage/gr-storage_mock';
-import {updatePreferences} from '../services/user/user-model';
-import {createDefaultPreferences} from '../constants/constants';
-import {appContext} from '../services/app-context';
+import {
+ DependencyRequestEvent,
+ DependencyError,
+ DependencyToken,
+ Provider,
+} from '../models/dependency';
declare global {
interface Window {
@@ -53,6 +62,7 @@
fixture: typeof fixtureImpl;
stub: typeof stubImpl;
sinon: typeof sinon;
+ chai: typeof chai;
}
let assert: typeof chai.assert;
let expect: typeof chai.expect;
@@ -61,6 +71,7 @@
}
window.assert = chai.assert;
window.expect = chai.expect;
+window.chai.use(chaiDomDiff);
window.sinon = sinon;
@@ -93,6 +104,39 @@
window.fixture = fixtureImpl;
let testSetupTimestampMs = 0;
+let appContext: AppContext & Finalizable;
+
+const injectedDependencies: Map<
+ DependencyToken<unknown>,
+ Provider<unknown>
+> = new Map();
+
+const finalizers: Finalizable[] = [];
+
+function injectDependency<T>(
+ dependency: DependencyToken<T>,
+ creator: Creator<T>
+) {
+ let service: (T & Finalizable) | undefined = undefined;
+ injectedDependencies.set(dependency, () => {
+ if (service) return service;
+ service = creator();
+ finalizers.push(service);
+ return service;
+ });
+}
+
+function resolveDependency(evt: DependencyRequestEvent<unknown>) {
+ const provider = injectedDependencies.get(evt.dependency);
+ if (provider) {
+ evt.callback(provider());
+ } else {
+ throw new DependencyError(
+ evt.dependency,
+ 'Forgot to set up dependency for tests'
+ );
+ }
+}
setup(() => {
testSetupTimestampMs = new Date().getTime();
@@ -101,11 +145,18 @@
// If the following asserts fails - then window.stub is
// overwritten by some other code.
assert.equal(getCleanupsCount(), 0);
- _testOnlyInitAppContext();
+ appContext = createTestAppContext();
+ injectAppContext(appContext);
+ finalizers.push(appContext);
+ const dependencies = createTestDependencies(appContext);
+ for (const [token, provider] of dependencies) {
+ injectDependency(token, provider);
+ }
+ document.addEventListener('request-dependency', resolveDependency);
// The following calls is nessecary to avoid influence of previously executed
// tests.
- initGlobalVariables();
- _testOnly_initGerritPluginApi();
+ initGlobalVariables(appContext);
+
const shortcuts = appContext.shortcutsService;
assert.isTrue(shortcuts._testOnly_isEmpty());
const selection = document.getSelection();
@@ -201,8 +252,12 @@
removeThemeStyles();
cancelAllTasks();
cleanUpStorage();
+ document.removeEventListener('request-dependency', resolveDependency);
+ injectedDependencies.clear();
// Reset state
- updatePreferences(createDefaultPreferences());
+ for (const f of finalizers) {
+ f.finalize();
+ }
const testTeardownTimestampMs = new Date().getTime();
const elapsedMs = testTeardownTimestampMs - testSetupTimestampMs;
if (elapsedMs > 1000) {
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 3bb0c34..28da1aa 100644
--- a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
+++ b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
@@ -134,9 +134,6 @@
return Promise.resolve(new Response());
},
deleteAccountSSHKey(): void {},
- deleteAssignee(): Promise<Response> {
- return Promise.resolve(new Response());
- },
deleteChangeCommitMessage(): Promise<Response> {
return Promise.resolve(new Response());
},
@@ -173,6 +170,7 @@
executeChangeAction(): Promise<Response | undefined> {
return Promise.resolve(new Response());
},
+ finalize(): void {},
generateAccountHttpPassword(): Promise<Password> {
return Promise.resolve('asdf');
},
@@ -227,11 +225,14 @@
getChangeConflicts(): Promise<ChangeInfo[] | undefined> {
return Promise.resolve([]);
},
- getChangeDetail(): Promise<ParsedChangeInfo | null | undefined> {
+ getChangeDetail(
+ changeNum?: number | string
+ ): Promise<ParsedChangeInfo | undefined> {
+ if (changeNum === undefined) return Promise.resolve(undefined);
return Promise.resolve(createChange() as ParsedChangeInfo);
},
- getChangeEdit(): Promise<false | EditInfo | undefined> {
- return Promise.resolve(false);
+ getChangeEdit(): Promise<EditInfo | undefined> {
+ return Promise.resolve(undefined);
},
getChangeFiles(): Promise<FileNameToFileInfoMap | undefined> {
return Promise.resolve({});
@@ -275,20 +276,23 @@
getDiff(): Promise<DiffInfo | undefined> {
throw new Error('getDiff() not implemented by RestApiMock.');
},
- getDiffChangeDetail(): Promise<ChangeInfo | undefined | null> {
- throw new Error('getDiffChangeDetail() not implemented by RestApiMock.');
- },
getDiffComments() {
- throw new Error('getDiffComments() not implemented by RestApiMock.');
+ // NOTE: This method can not be typed properly due to overloads.
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return Promise.resolve({}) as any;
},
getDiffDrafts() {
- throw new Error('getDiffDrafts() not implemented by RestApiMock.');
+ // NOTE: This method can not be typed properly due to overloads.
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return Promise.resolve({}) as any;
},
getDiffPreferences(): Promise<DiffPreferencesInfo | undefined> {
return Promise.resolve(createDefaultDiffPrefs());
},
getDiffRobotComments() {
- throw new Error('getDiffRobotComments() not implemented by RestApiMock.');
+ // NOTE: This method can not be typed properly due to overloads.
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return Promise.resolve({}) as any;
},
getDocumentationSearches(): Promise<DocResult[] | undefined> {
return Promise.resolve([]);
@@ -506,9 +510,6 @@
setAccountUsername(): Promise<void> {
return Promise.resolve();
},
- setAssignee(): Promise<Response> {
- return Promise.resolve(new Response());
- },
setChangeHashtag(): Promise<Hashtag[]> {
return Promise.resolve([]);
},
diff --git a/polygerrit-ui/app/test/test-app-context-init.ts b/polygerrit-ui/app/test/test-app-context-init.ts
index 483baa6..2f39b3c 100644
--- a/polygerrit-ui/app/test/test-app-context-init.ts
+++ b/polygerrit-ui/app/test/test-app-context-init.ts
@@ -16,28 +16,102 @@
*/
// Init app context before any other imports
-import {initAppContext} from '../services/app-context-init';
+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 {AppContext, appContext} from '../services/app-context';
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} from '../services/change/change-model';
+import {ChecksModel} from '../services/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} from '../services/shortcuts/shortcuts-service';
+import {ConfigModel, configModelToken} from '../models/config/config-model';
+import {BrowserModel, browserModelToken} from '../models/browser/browser-model';
-export function _testOnlyInitAppContext() {
- initAppContext();
+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);
+ },
+ restApiService: (_ctx: Partial<AppContext>) => grRestApiMock,
+ changeModel: (ctx: Partial<AppContext>) => {
+ const routerModel = ctx.routerModel;
+ const restApiService = ctx.restApiService;
+ const userModel = ctx.userModel;
+ assertIsDefined(routerModel, 'routerModel');
+ assertIsDefined(restApiService, 'restApiService');
+ assertIsDefined(userModel, 'userModel');
+ return new ChangeModel(routerModel, restApiService, userModel);
+ },
+ checksModel: (ctx: Partial<AppContext>) => {
+ const routerModel = ctx.routerModel;
+ const changeModel = ctx.changeModel;
+ const reportingService = ctx.reportingService;
+ assertIsDefined(routerModel, 'routerModel');
+ assertIsDefined(changeModel, 'changeModel');
+ assertIsDefined(reportingService, 'reportingService');
+ return new ChecksModel(routerModel, changeModel, reportingService);
+ },
+ 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!);
+ },
+ shortcutsService: (ctx: Partial<AppContext>) => {
+ assertIsDefined(ctx.userModel, 'userModel');
+ assertIsDefined(ctx.reportingService, 'reportingService');
+ return new ShortcutsService(ctx.userModel!, ctx.reportingService!);
+ },
+ };
+ return create<AppContext>(appRegistry);
+}
- function setMock<T extends keyof AppContext>(
- serviceName: T,
- setupMock: AppContext[T]
- ) {
- Object.defineProperty(appContext, serviceName, {
- get() {
- return setupMock;
- },
- });
- }
- setMock('reportingService', grReportingMock);
- setMock('restApiService', grRestApiMock);
- setMock('storageService', grStorageMock);
- setMock('authService', new GrAuthMock(appContext.eventEmitter));
+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
+): Map<DependencyToken<unknown>, Creator<unknown>> {
+ const dependencies = new Map();
+ const browserModel = () => new BrowserModel(appContext.userModel!);
+ dependencies.set(browserModelToken, browserModel);
+
+ const commentsModel = () =>
+ new CommentsModel(
+ appContext.routerModel,
+ appContext.changeModel,
+ appContext.restApiService,
+ appContext.reportingService
+ );
+ dependencies.set(commentsModelToken, commentsModel);
+
+ const configModel = () =>
+ new ConfigModel(appContext.changeModel, appContext.restApiService);
+ dependencies.set(configModelToken, configModel);
+
+ return dependencies;
}
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 351cc13..00b081b 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -14,7 +14,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
import {
AccountDetailInfo,
AccountId,
@@ -31,12 +30,14 @@
ChangeMessageId,
ChangeMessageInfo,
ChangeViewChangeInfo,
+ CommentInfo,
CommentLinkInfo,
CommentLinks,
CommitId,
CommitInfo,
ConfigInfo,
DownloadInfo,
+ EditInfo,
EditPatchSetNum,
EmailAddress,
FixId,
@@ -62,6 +63,9 @@
RequirementType,
Reviewers,
RevisionInfo,
+ RobotCommentInfo,
+ RobotId,
+ RobotRunId,
SchemesInfoMap,
ServerInfo,
SubmittedTogetherInfo,
@@ -92,17 +96,29 @@
} from '../constants/constants';
import {formatDate} from '../utils/date-util';
import {GetDiffCommentsOutput} from '../services/gr-rest-api/gr-rest-api';
-import {AppElementChangeViewParams} from '../elements/gr-app-types';
+import {
+ AppElementChangeViewParams,
+ AppElementSearchParam,
+} from '../elements/gr-app-types';
import {CommitInfoWithRequiredCommit} from '../elements/change/gr-change-metadata/gr-change-metadata';
import {WebLinkInfo} from '../types/diff';
-import {createCommentThreads, UIComment, UIDraft} from '../utils/comment-util';
+import {
+ CommentThread,
+ createCommentThreads,
+ DraftInfo,
+ UnsavedInfo,
+} from '../utils/comment-util';
import {GerritView} from '../services/router/router-model';
import {ChangeComments} from '../elements/diff/gr-comment-api/gr-comment-api';
import {EditRevisionInfo, ParsedChangeInfo} from '../types/types';
import {ChangeMessage} from '../elements/change/gr-message/gr-message';
-import {GenerateUrlEditViewParameters} from '../elements/core/gr-navigation/gr-navigation';
+import {
+ GenerateUrlEditViewParameters,
+ GenerateUrlTopicViewParams,
+} from '../elements/core/gr-navigation/gr-navigation';
import {
DetailedLabelInfo,
+ QuickLabelInfo,
SubmitRequirementExpressionInfo,
SubmitRequirementResultInfo,
SubmitRequirementStatus,
@@ -155,6 +171,7 @@
work_in_progress_by_default: createInheritedBoolean(),
max_object_size_limit: createMaxObjectSizeLimit(),
default_submit_type: createSubmitType(),
+ enable_reviewer_by_email: createInheritedBoolean(),
submit_type: SubmitType.INHERIT,
commentlinks: createCommentLinks(),
};
@@ -230,7 +247,10 @@
};
}
-export function createRevision(patchSetNum = 1): RevisionInfo {
+export function createRevision(
+ patchSetNum = 1,
+ description = ''
+): RevisionInfo {
return {
_number: patchSetNum as PatchSetNum,
commit: createCommit(),
@@ -238,14 +258,29 @@
kind: RevisionKind.REWORK,
ref: 'refs/changes/5/6/1' as GitRef,
uploader: createAccountWithId(),
+ description,
};
}
-export function createEditRevision(): EditRevisionInfo {
+export function createEditInfo(): EditInfo {
+ return {
+ commit: {...createCommit(), commit: 'commit-id-of-edit-ps' as CommitId},
+ base_patch_set_number: 1 as BasePatchSetNum,
+ base_revision: 'base-revision-of-edit',
+ ref: 'refs/changes/5/6/1' as GitRef,
+ fetch: {},
+ files: {},
+ };
+}
+
+export function createEditRevision(basePatchNum = 1): EditRevisionInfo {
return {
_number: EditPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- commit: createCommit(),
+ basePatchNum: basePatchNum as BasePatchSetNum,
+ commit: {
+ ...createCommit(),
+ commit: 'test-commit-id-of-edit-rev' as CommitId,
+ },
};
}
@@ -270,7 +305,7 @@
[revisionId: string]: RevisionInfo;
} {
const revisions: {[revisionId: string]: RevisionInfo} = {};
- const revisionDate = TEST_CHANGE_CREATED;
+ let revisionDate = TEST_CHANGE_CREATED;
const revisionIdStart = 1; // The same as getCurrentRevision
for (let i = 0; i < count; i++) {
const revisionId = (i + revisionIdStart).toString(16);
@@ -281,6 +316,7 @@
};
revisions[revisionId] = revision;
// advance 1 day
+ revisionDate = new Date(revisionDate);
revisionDate.setDate(revisionDate.getDate() + 1);
}
return revisions;
@@ -294,12 +330,13 @@
export function createChangeMessages(count: number): ChangeMessageInfo[] {
const messageIdStart = 1000;
const messages: ChangeMessageInfo[] = [];
- const messageDate = TEST_CHANGE_CREATED;
+ let messageDate = TEST_CHANGE_CREATED;
for (let i = 0; i < count; i++) {
messages.push({
...createChangeMessageInfo((i + messageIdStart).toString(16)),
date: dateToTimestamp(messageDate),
});
+ messageDate = new Date(messageDate);
messageDate.setDate(messageDate.getDate() + 1);
}
return messages;
@@ -360,7 +397,6 @@
update_delay: 0,
mergeability_computation_behavior:
MergeabilityComputationBehavior.REF_UPDATED_AND_CHANGE_REINDEX,
- enable_assignee: false,
};
}
@@ -456,6 +492,14 @@
};
}
+export function createAppElementSearchViewParams(): AppElementSearchParam {
+ return {
+ view: GerritView.SEARCH,
+ query: TEST_NUMERIC_CHANGE_ID.toString(),
+ offset: '0',
+ };
+}
+
export function createGenerateUrlEditViewParameters(): GenerateUrlEditViewParameters {
return {
view: GerritView.EDIT,
@@ -466,6 +510,13 @@
};
}
+export function createGenerateUrlTopicViewParams(): GenerateUrlTopicViewParams {
+ return {
+ view: GerritView.TOPIC,
+ topic: 'myTopic',
+ };
+}
+
export function createRequirement(): Requirement {
return {
status: RequirementStatus.OK,
@@ -482,7 +533,9 @@
};
}
-export function createComment(): UIComment {
+export function createComment(
+ extra: Partial<CommentInfo | DraftInfo> = {}
+): CommentInfo {
return {
patch_set: 1 as PatchSetNum,
id: '12345' as UrlEncodedCommentId,
@@ -492,15 +545,38 @@
updated: '2018-02-13 22:48:48.018000000' as Timestamp,
unresolved: false,
path: 'abc.txt',
+ ...extra,
};
}
-export function createDraft(): UIDraft {
+export function createDraft(extra: Partial<CommentInfo> = {}): DraftInfo {
return {
...createComment(),
- collapsed: false,
__draft: true,
- __editing: false,
+ ...extra,
+ };
+}
+
+export function createUnsaved(extra: Partial<CommentInfo> = {}): UnsavedInfo {
+ return {
+ ...createComment(),
+ __unsaved: true,
+ id: undefined,
+ updated: undefined,
+ ...extra,
+ };
+}
+
+export function createRobotComment(
+ extra: Partial<CommentInfo> = {}
+): RobotCommentInfo {
+ return {
+ ...createComment(),
+ robot_id: 'robot-id-123' as RobotId,
+ robot_run_id: 'robot-run-id-456' as RobotRunId,
+ properties: {},
+ fix_suggestions: [],
+ ...extra,
};
}
@@ -607,14 +683,27 @@
return new ChangeComments(comments, {}, drafts, {}, {});
}
-export function createCommentThread(comments: UIComment[]) {
+export function createThread(
+ ...comments: Partial<CommentInfo | DraftInfo>[]
+): CommentThread {
+ return {
+ comments: comments.map(c => createComment(c)),
+ rootId: 'test-root-id-comment-thread' as UrlEncodedCommentId,
+ path: 'test-path-comment-thread',
+ commentSide: CommentSide.REVISION,
+ patchNum: 1 as PatchSetNum,
+ line: 314,
+ };
+}
+
+export function createCommentThread(comments: Array<Partial<CommentInfo>>) {
if (!comments.length) {
throw new Error('comment is required to create a thread');
}
- comments = comments.map(comment => {
+ const filledComments = comments.map(comment => {
return {...createComment(), ...comment};
});
- const threads = createCommentThreads(comments);
+ const threads = createCommentThreads(filledComments);
return threads[0];
}
@@ -689,6 +778,7 @@
name: 'Verified',
status: SubmitRequirementStatus.SATISFIED,
submittability_expression_result: createSubmitRequirementExpressionInfo(),
+ is_legacy: false,
};
}
@@ -717,3 +807,7 @@
},
};
}
+
+export function createQuickLabelInfo(): QuickLabelInfo {
+ return {};
+}
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 557fe71..88167e0 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -17,28 +17,32 @@
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 {appContext} from '../services/app-context';
+import {getAppContext} from '../services/app-context';
import {RestApiService} from '../services/gr-rest-api/gr-rest-api';
-import {SinonSpy} from 'sinon';
+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 {CommentsService} from '../services/comments/comments-service';
-import {UserService} from '../services/user/user-service';
+import {UserModel} from '../models/user/user-model';
+import {ShortcutsService} from '../services/shortcuts/shortcuts-service';
+import {queryAndAssert, query} from '../utils/common-util';
+import {FlagsService} from '../services/flags/flags';
+import {Key, Modifier} from '../utils/dom-util';
+import {ChangeModel} from '../services/change/change-model';
export {query, queryAll, queryAndAssert} from '../utils/common-util';
-export interface MockPromise extends Promise<unknown> {
- resolve: (value?: unknown) => void;
+export interface MockPromise<T> extends Promise<T> {
+ resolve: (value?: T) => void;
}
-export const mockPromise = () => {
- let res: (value?: unknown) => void;
- const promise: MockPromise = new Promise(resolve => {
+export function mockPromise<T = unknown>(): MockPromise<T> {
+ let res: (value?: T) => void;
+ const promise: MockPromise<T> = new Promise<T | undefined>(resolve => {
res = resolve;
- }) as MockPromise;
+ }) as MockPromise<T>;
promise.resolve = res!;
return promise;
-};
+}
export function isHidden(el: Element | undefined | null) {
if (!el) return true;
@@ -101,35 +105,43 @@
}
export function stubRestApi<K extends keyof RestApiService>(method: K) {
- return sinon.stub(appContext.restApiService, method);
+ return sinon.stub(getAppContext().restApiService, method);
}
export function spyRestApi<K extends keyof RestApiService>(method: K) {
- return sinon.spy(appContext.restApiService, method);
+ return sinon.spy(getAppContext().restApiService, method);
}
-export function stubComments<K extends keyof CommentsService>(method: K) {
- return sinon.stub(appContext.commentsService, method);
+export function stubChange<K extends keyof ChangeModel>(method: K) {
+ return sinon.stub(getAppContext().changeModel, method);
}
-export function stubUsers<K extends keyof UserService>(method: K) {
- return sinon.stub(appContext.userService, method);
+export function stubUsers<K extends keyof UserModel>(method: K) {
+ return sinon.stub(getAppContext().userModel, method);
+}
+
+export function stubShortcuts<K extends keyof ShortcutsService>(method: K) {
+ return sinon.stub(getAppContext().shortcutsService, method);
}
export function stubStorage<K extends keyof StorageService>(method: K) {
- return sinon.stub(appContext.storageService, method);
+ return sinon.stub(getAppContext().storageService, method);
}
export function spyStorage<K extends keyof StorageService>(method: K) {
- return sinon.spy(appContext.storageService, method);
+ return sinon.spy(getAppContext().storageService, method);
}
export function stubAuth<K extends keyof AuthService>(method: K) {
- return sinon.stub(appContext.authService, method);
+ return sinon.stub(getAppContext().authService, method);
}
export function stubReporting<K extends keyof ReportingService>(method: K) {
- return sinon.stub(appContext.reportingService, method);
+ return sinon.stub(getAppContext().reportingService, method);
+}
+
+export function stubFlags<K extends keyof FlagsService>(method: K) {
+ return sinon.stub(getAppContext().flagsService, method);
}
export type SinonSpyMember<F extends (...args: any) => any> = SinonSpy<
@@ -162,27 +174,44 @@
document.head.querySelector('#dark-theme')?.remove();
}
+export async function waitQueryAndAssert<E extends Element = Element>(
+ el: Element | null | undefined,
+ selector: string
+): Promise<E> {
+ await waitUntil(
+ () => !!query<E>(el, selector),
+ `The element '${selector}' did not appear in the DOM within 1000 ms.`
+ );
+ return queryAndAssert<E>(el, selector);
+}
+
export function waitUntil(
predicate: () => boolean,
- maxMillis = 100
+ message = 'The waitUntil() predicate is still false after 1000 ms.'
): Promise<void> {
const start = Date.now();
- let sleep = 1;
+ let sleep = 0;
+ if (predicate()) return Promise.resolve();
+ const error = new Error(message);
return new Promise((resolve, reject) => {
const waiter = () => {
if (predicate()) {
return resolve();
}
- if (Date.now() - start >= maxMillis) {
- return reject(new Error('Took to long to waitUntil'));
+ if (Date.now() - start >= 1000) {
+ return reject(error);
}
setTimeout(waiter, sleep);
- sleep *= 2;
+ sleep = sleep === 0 ? 1 : sleep * 4;
};
waiter();
});
}
+export function waitUntilCalled(stub: SinonStub | SinonSpy, name: string) {
+ return waitUntil(() => stub.called, `${name} was not called`);
+}
+
/**
* Promisify an event callback to simplify async...await tests.
*
@@ -190,17 +219,46 @@
* await listenOnce(el, 'render');
* ...
*/
-export function listenOnce(el: EventTarget, eventType: string) {
- return new Promise<void>(resolve => {
- const listener = () => {
+export function listenOnce<T extends Event>(
+ el: EventTarget,
+ eventType: string
+) {
+ return new Promise<T>(resolve => {
+ const listener = (e: Event) => {
removeEventListener();
- resolve();
+ resolve(e as T);
};
- el.addEventListener(eventType, listener);
let removeEventListener = () => {
el.removeEventListener(eventType, listener);
removeEventListener = () => {};
};
+ el.addEventListener(eventType, listener);
registerTestCleanup(removeEventListener);
});
}
+
+export function dispatch<T>(element: HTMLElement, type: string, detail: T) {
+ const eventOptions = {
+ detail,
+ bubbles: true,
+ composed: true,
+ };
+ element.dispatchEvent(new CustomEvent<T>(type, eventOptions));
+}
+
+export function pressKey(
+ element: HTMLElement,
+ key: string | Key,
+ ...modifiers: Modifier[]
+) {
+ const eventOptions = {
+ key,
+ bubbles: true,
+ composed: true,
+ altKey: modifiers.includes(Modifier.ALT_KEY),
+ ctrlKey: modifiers.includes(Modifier.CTRL_KEY),
+ metaKey: modifiers.includes(Modifier.META_KEY),
+ shiftKey: modifiers.includes(Modifier.SHIFT_KEY),
+ };
+ element.dispatchEvent(new KeyboardEvent('keydown', eventOptions));
+}
diff --git a/polygerrit-ui/app/tsconfig.json b/polygerrit-ui/app/tsconfig.json
index 5040496..a335926 100644
--- a/polygerrit-ui/app/tsconfig.json
+++ b/polygerrit-ui/app/tsconfig.json
@@ -87,6 +87,7 @@
"embed/**/*",
"gr-diff/**/*",
"mixins/**/*",
+ "models/**/*",
"samples/**/*",
"scripts/**/*",
"services/**/*",
diff --git a/polygerrit-ui/app/tsconfig_bazel.json b/polygerrit-ui/app/tsconfig_bazel.json
index dfd2078..cd83fc0 100644
--- a/polygerrit-ui/app/tsconfig_bazel.json
+++ b/polygerrit-ui/app/tsconfig_bazel.json
@@ -16,6 +16,7 @@
"embed/**/*",
"gr-diff/**/*",
"mixins/**/*",
+ "models/**/*",
"samples/**/*",
"scripts/**/*",
"services/**/*",
diff --git a/polygerrit-ui/app/tsconfig_bazel_test.json b/polygerrit-ui/app/tsconfig_bazel_test.json
index 7137e23..be3c934 100644
--- a/polygerrit-ui/app/tsconfig_bazel_test.json
+++ b/polygerrit-ui/app/tsconfig_bazel_test.json
@@ -20,6 +20,7 @@
"embed/**/*",
"gr-diff/**/*",
"mixins/**/*",
+ "models/**/*",
"samples/**/*",
"scripts/**/*",
"services/**/*",
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 1617aa3..f58abb3 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -206,6 +206,7 @@
UrlEncodedRepoName,
UserConfigInfo,
VotingRangeInfo,
+ WebLinkInfo,
isDetailedLabelInfo,
isQuickLabelInfo,
};
@@ -691,9 +692,10 @@
* https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#comment-info
*/
export interface CommentInfo {
- // TODO(TS): Make this required.
- patch_set?: PatchSetNum;
id: UrlEncodedCommentId;
+ updated: Timestamp;
+ // TODO(TS): Make this required. Every comment must have patch_set set.
+ patch_set?: PatchSetNum;
path?: string;
side?: CommentSide;
parent?: number;
@@ -701,7 +703,6 @@
range?: CommentRange;
in_reply_to?: UrlEncodedCommentId;
message?: string;
- updated: Timestamp;
author?: AccountInfo;
tag?: string;
unresolved?: boolean;
@@ -963,14 +964,6 @@
}
/**
- * The AssigneeInput entity contains the identity of the user to be set as assignee
- * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#assignee-input
- */
-export interface AssigneeInput {
- assignee: AccountId;
-}
-
-/**
* The SshKeyInfo entity contains information about an SSH key of a user
* https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#ssh-key-info
*/
@@ -1147,8 +1140,6 @@
work_in_progress_by_default?: boolean;
// The email_format doesn't mentioned in doc, but exists in Java class GeneralPreferencesInfo
email_format?: EmailFormat;
- // The following property doesn't exist in RestAPI, it is added by GrRestApiInterface
- default_diff_view?: DiffViewMode;
}
/**
diff --git a/polygerrit-ui/app/types/diff.ts b/polygerrit-ui/app/types/diff.ts
index 223f290..562d47f 100644
--- a/polygerrit-ui/app/types/diff.ts
+++ b/polygerrit-ui/app/types/diff.ts
@@ -52,9 +52,6 @@
/** Meta information about the file on side B as a DiffFileMetaInfo entity. */
meta_b: DiffFileMetaInfo;
- /** A list of strings representing the patch set diff header. */
- diff_header?: string[];
-
/**
* Links to the file diff in external sites as a list of DiffWebLinkInfo
* entries.
@@ -98,7 +95,7 @@
export interface DiffPreferencesInfo extends DiffPreferenceInfoApi {
expand_all_comments?: boolean;
- cursor_blink_rate: number;
+ cursor_blink_rate?: number;
manual_review?: boolean;
retain_header?: boolean;
skip_deleted?: boolean;
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index b6376c4..4f24535 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -15,13 +15,14 @@
* limitations under the License.
*/
import {PatchSetNum} from './common';
-import {UIComment} from '../utils/comment-util';
+import {Comment} from '../utils/comment-util';
import {FetchRequest} from './types';
import {LineNumberEventDetail, MovedLinkClickedEventDetail} from '../api/diff';
import {Category, RunStatus} from '../api/checks';
import {ChangeMessage} from '../elements/change/gr-message/gr-message';
export enum EventType {
+ BIND_VALUE_CHANGED = 'bind-value-changed',
CHANGE = 'change',
CHANGED = 'changed',
CHANGE_MESSAGE_DELETED = 'change-message-deleted',
@@ -56,6 +57,8 @@
declare global {
interface HTMLElementEventMap {
/* prettier-ignore */
+ 'bind-value-changed': BindValueChangeEvent;
+ /* prettier-ignore */
'change': ChangeEvent;
/* prettier-ignore */
'changed': ChangedEvent;
@@ -102,6 +105,11 @@
}
}
+export interface BindValueChangeEventDetail {
+ value: string;
+}
+export type BindValueChangeEvent = CustomEvent<BindValueChangeEventDetail>;
+
export type ChangeEvent = InputEvent;
export type ChangedEvent = CustomEvent<string>;
@@ -160,7 +168,7 @@
export interface OpenFixPreviewEventDetail {
patchNum?: PatchSetNum;
- comment?: UIComment;
+ comment?: Comment;
}
export type OpenFixPreviewEvent = CustomEvent<OpenFixPreviewEventDetail>;
@@ -170,7 +178,7 @@
export type CloseFixPreviewEvent = CustomEvent<CloseFixPreviewEventDetail>;
export interface CreateFixCommentEventDetail {
patchNum?: PatchSetNum;
- comment?: UIComment;
+ comment?: Comment;
}
export type CreateFixCommentEvent = CustomEvent<CreateFixCommentEventDetail>;
@@ -241,3 +249,12 @@
title: string;
}
export type TitleChangeEvent = CustomEvent<TitleChangeEventDetail>;
+
+/**
+ * This event can be used for Polymer properties that have `notify: true` set.
+ * But it is also generally recommended when you want to notify your parent
+ * elements about a property update, also for Lit elements.
+ *
+ * The name of the event should be `prop-name-changed`.
+ */
+export type ValueChangedEvent<T = string> = CustomEvent<{value: T}>;
diff --git a/polygerrit-ui/app/types/types.ts b/polygerrit-ui/app/types/types.ts
index 2d4d412..c6137eb 100644
--- a/polygerrit-ui/app/types/types.ts
+++ b/polygerrit-ui/app/types/types.ts
@@ -171,7 +171,9 @@
}
export type DiffLayerListener = (
+ /** 1-based inclusive */
start: number,
+ /** 1-based inclusive */
end: number,
side: Side
) => void;
@@ -186,10 +188,8 @@
patchRange: PatchRange | null;
selectedFileIndex: number;
showReplyDialog: boolean;
- showDownloadDialog: boolean;
diffMode: DiffViewMode | null;
numFilesShown: number | null;
- diffViewMode?: boolean;
}
export interface ChangeListViewState {
@@ -199,7 +199,6 @@
selectedFileIndex?: number;
selectedChangeIndex?: number;
showReplyDialog?: boolean;
- showDownloadDialog?: boolean;
diffMode?: DiffViewMode;
numFilesShown?: number;
scrollTop?: number;
diff --git a/polygerrit-ui/app/utils/access-util.ts b/polygerrit-ui/app/utils/access-util.ts
index 165eacf..e66bbb1 100644
--- a/polygerrit-ui/app/utils/access-util.ts
+++ b/polygerrit-ui/app/utils/access-util.ts
@@ -25,7 +25,6 @@
DELETE = 'delete',
DELETE_CHANGES = 'deleteChanges',
DELETE_OWN_CHANGES = 'deleteOwnChanges',
- EDIT_ASSIGNEE = 'editAssignee',
EDIT_HASHTAGS = 'editHashtags',
EDIT_TOPIC_NAME = 'editTopicName',
FORGE_AUTHOR = 'forgeAuthor',
@@ -79,10 +78,6 @@
id: AccessPermissionId.DELETE_OWN_CHANGES,
name: 'Delete Own Changes',
},
- [AccessPermissionId.EDIT_ASSIGNEE]: {
- id: AccessPermissionId.EDIT_ASSIGNEE,
- name: 'Edit Assignee',
- },
[AccessPermissionId.EDIT_HASHTAGS]: {
id: AccessPermissionId.EDIT_HASHTAGS,
name: 'Edit Hashtags',
diff --git a/polygerrit-ui/app/utils/admin-nav-util.ts b/polygerrit-ui/app/utils/admin-nav-util.ts
index 8bd22ef..6688c19 100644
--- a/polygerrit-ui/app/utils/admin-nav-util.ts
+++ b/polygerrit-ui/app/utils/admin-nav-util.ts
@@ -34,7 +34,7 @@
name: 'Repositories',
noBaseUrl: true,
url: '/admin/repos',
- view: 'gr-repo-list',
+ view: 'gr-repo-list' as GerritView,
viewableToAll: true,
},
{
@@ -42,7 +42,7 @@
section: 'Groups',
noBaseUrl: true,
url: '/admin/groups',
- view: 'gr-admin-group-list',
+ view: 'gr-admin-group-list' as GerritView,
},
{
name: 'Plugins',
@@ -50,7 +50,7 @@
section: 'Plugins',
noBaseUrl: true,
url: '/admin/plugins',
- view: 'gr-plugin-list',
+ view: 'gr-plugin-list' as GerritView,
},
];
@@ -107,7 +107,7 @@
name: link.text,
capability: link.capability || undefined,
noBaseUrl: !isExternalLink(link),
- view: null,
+ view: undefined,
viewableToAll: !link.capability,
target: isExternalLink(link) ? '_blank' : null,
};
@@ -252,10 +252,11 @@
name: string;
noBaseUrl: boolean;
url: string;
- view: string | null;
+ view?: GerritView;
viewableToAll?: boolean;
section?: string;
capability?: string;
target?: string | null;
subsection?: SubsectionInterface;
+ children?: SubsectionInterface[];
}
diff --git a/polygerrit-ui/app/utils/async-util.ts b/polygerrit-ui/app/utils/async-util.ts
index 90ee5a5..3f51532 100644
--- a/polygerrit-ui/app/utils/async-util.ts
+++ b/polygerrit-ui/app/utils/async-util.ts
@@ -15,6 +15,9 @@
* limitations under the License.
*/
+import {Observable} from 'rxjs';
+import {filter, take} from 'rxjs/operators';
+
/**
* @param fn An iteratee function to be passed each element of
* the array in order. Must return a promise, and the following
@@ -130,3 +133,16 @@
fn(e);
};
}
+
+/**
+ * Let's you wait for an Observable to become true.
+ */
+export function until<T>(obs$: Observable<T>, predicate: (t: T) => boolean) {
+ return new Promise<void>(resolve => {
+ obs$.pipe(filter(predicate), take(1)).subscribe(() => {
+ resolve();
+ });
+ });
+}
+
+export const isFalse = (b: boolean) => b === false;
diff --git a/polygerrit-ui/app/utils/change-metadata-util.ts b/polygerrit-ui/app/utils/change-metadata-util.ts
index 9692ab31..84b324b 100644
--- a/polygerrit-ui/app/utils/change-metadata-util.ts
+++ b/polygerrit-ui/app/utils/change-metadata-util.ts
@@ -33,7 +33,6 @@
UPLOADER = 'Uploader',
AUTHOR = 'Author',
COMMITTER = 'Committer',
- ASSIGNEE = 'Assignee',
CHERRY_PICK_OF = 'Cherry pick of',
}
@@ -51,7 +50,6 @@
Metadata.UPLOADER,
Metadata.AUTHOR,
Metadata.COMMITTER,
- Metadata.ASSIGNEE,
Metadata.CHERRY_PICK_OF,
],
ALWAYS_HIDE: [
@@ -74,7 +72,6 @@
case Metadata.UPLOADER:
case Metadata.AUTHOR:
case Metadata.COMMITTER:
- case Metadata.ASSIGNEE:
return false;
case Metadata.CHERRY_PICK_OF:
return !!change?.cherry_pick_of_change;
diff --git a/polygerrit-ui/app/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts
index 5b08fab..ee26915 100644
--- a/polygerrit-ui/app/utils/comment-util.ts
+++ b/polygerrit-ui/app/utils/comment-util.ts
@@ -30,84 +30,116 @@
AccountInfo,
AccountDetailInfo,
} from '../types/common';
-import {CommentSide, Side, SpecialFilePath} from '../constants/constants';
+import {CommentSide, SpecialFilePath} from '../constants/constants';
import {parseDate} from './date-util';
-import {LineNumber} from '../elements/diff/gr-diff/gr-diff-line';
import {CommentIdToCommentThreadMap} from '../elements/diff/gr-comment-api/gr-comment-api';
import {isMergeParent, getParentIndex} from './patch-set-util';
import {DiffInfo} from '../types/diff';
+import {LineNumber} from '../api/diff';
export interface DraftCommentProps {
- __draft?: boolean;
- __draftID?: string;
- __date?: Date;
+ // This must be true for all drafts. Drafts received from the backend will be
+ // modified immediately with __draft:true before allowing them to get into
+ // the application state.
+ __draft: boolean;
}
-export type DraftInfo = CommentBasics & DraftCommentProps;
-
-/**
- * Each of the type implements or extends CommentBasics.
- */
-export type Comment = DraftInfo | CommentInfo | RobotCommentInfo;
-
-export interface UIStateCommentProps {
- collapsed?: boolean;
+export interface UnsavedCommentProps {
+ // This must be true for all unsaved comment drafts. An unsaved draft is
+ // always just local to a comment component like <gr-comment> or
+ // <gr-comment-thread>. Unsaved drafts will never appear in the application
+ // state.
+ __unsaved: boolean;
}
-export interface UIStateDraftProps {
- __editing?: boolean;
-}
+export type DraftInfo = CommentInfo & DraftCommentProps;
-export type UIDraft = DraftInfo & UIStateCommentProps & UIStateDraftProps;
+export type UnsavedInfo = CommentBasics & UnsavedCommentProps;
-export type UIHuman = CommentInfo & UIStateCommentProps;
-
-export type UIRobot = RobotCommentInfo & UIStateCommentProps;
-
-export type UIComment = UIHuman | UIRobot | UIDraft;
+export type Comment = UnsavedInfo | DraftInfo | CommentInfo | RobotCommentInfo;
export type CommentMap = {[path: string]: boolean};
-export function isRobot<T extends CommentInfo>(
+export function isRobot<T extends CommentBasics>(
x: T | DraftInfo | RobotCommentInfo | undefined
): x is RobotCommentInfo {
return !!x && !!(x as RobotCommentInfo).robot_id;
}
-export function isDraft<T extends CommentInfo>(
- x: T | UIDraft | undefined
-): x is UIDraft {
- return !!x && !!(x as UIDraft).__draft;
+export function isDraft<T extends CommentBasics>(
+ x: T | DraftInfo | undefined
+): x is DraftInfo {
+ return !!x && !!(x as DraftInfo).__draft;
+}
+
+export function isUnsaved<T extends CommentBasics>(
+ x: T | UnsavedInfo | undefined
+): x is UnsavedInfo {
+ return !!x && !!(x as UnsavedInfo).__unsaved;
+}
+
+export function isDraftOrUnsaved<T extends CommentBasics>(
+ x: T | DraftInfo | UnsavedInfo | undefined
+): x is UnsavedInfo | DraftInfo {
+ return isDraft(x) || isUnsaved(x);
}
interface SortableComment {
- __draft?: boolean;
- __date?: Date;
- updated?: Timestamp;
- id?: UrlEncodedCommentId;
+ updated: Timestamp;
+ id: UrlEncodedCommentId;
}
export function sortComments<T extends SortableComment>(comments: T[]): T[] {
return comments.slice(0).sort((c1, c2) => {
- const d1 = !!c1.__draft;
- const d2 = !!c2.__draft;
+ const d1 = isDraft(c1);
+ const d2 = isDraft(c2);
if (d1 !== d2) return d1 ? 1 : -1;
- const date1 = (c1.updated && parseDate(c1.updated)) || c1.__date;
- const date2 = (c2.updated && parseDate(c2.updated)) || c2.__date;
+ const date1 = parseDate(c1.updated);
+ const date2 = parseDate(c2.updated);
const dateDiff = date1!.valueOf() - date2!.valueOf();
if (dateDiff !== 0) return dateDiff;
- const id1 = c1.id ?? '';
- const id2 = c2.id ?? '';
+ const id1 = c1.id;
+ const id2 = c2.id;
return id1.localeCompare(id2);
});
}
-export function createCommentThreads(
- comments: UIComment[],
- patchRange?: PatchRange
-) {
+export function createUnsavedComment(thread: CommentThread): UnsavedInfo {
+ return {
+ path: thread.path,
+ patch_set: thread.patchNum,
+ side: thread.commentSide ?? CommentSide.REVISION,
+ line: typeof thread.line === 'number' ? thread.line : undefined,
+ range: thread.range,
+ parent: thread.mergeParentNum,
+ message: '',
+ unresolved: true,
+ __unsaved: true,
+ };
+}
+
+export function createUnsavedReply(
+ replyingTo: CommentInfo,
+ message: string,
+ unresolved: boolean
+): UnsavedInfo {
+ return {
+ path: replyingTo.path,
+ patch_set: replyingTo.patch_set,
+ side: replyingTo.side,
+ line: replyingTo.line,
+ range: replyingTo.range,
+ parent: replyingTo.parent,
+ in_reply_to: replyingTo.id,
+ message,
+ unresolved,
+ __unsaved: true,
+ };
+}
+
+export function createCommentThreads(comments: CommentInfo[]) {
const sortedComments = sortComments(comments);
const threads: CommentThread[] = [];
const idThreadMap: CommentIdToCommentThreadMap = {};
@@ -129,7 +161,6 @@
const newThread: CommentThread = {
comments: [comment],
patchNum: comment.patch_set,
- diffSide: Side.LEFT,
commentSide: comment.side ?? CommentSide.REVISION,
mergeParentNum: comment.parent,
path: comment.path,
@@ -137,13 +168,6 @@
range: comment.range,
rootId: comment.id,
};
- if (patchRange) {
- if (isInBaseOfPatchRange(comment, patchRange))
- newThread.diffSide = Side.LEFT;
- else if (isInRevisionOfPatchRange(comment, patchRange))
- newThread.diffSide = Side.RIGHT;
- else throw new Error('comment does not belong in given patchrange');
- }
if (!comment.line && !comment.range) {
newThread.line = 'FILE';
}
@@ -154,68 +178,126 @@
}
export interface CommentThread {
- comments: UIComment[];
+ /**
+ * This can only contain at most one draft. And if so, then it is the last
+ * comment in this list. This must not contain unsaved drafts.
+ */
+ comments: Array<CommentInfo | DraftInfo | RobotCommentInfo>;
+ /**
+ * Identical to the id of the first comment. If this is undefined, then the
+ * thread only contains an unsaved draft.
+ */
+ rootId?: UrlEncodedCommentId;
+ /**
+ * Note that all location information is typically identical to that of the
+ * first comment, but not for ported comments!
+ */
path: string;
commentSide: CommentSide;
/* mergeParentNum is the merge parent number only valid for merge commits
when commentSide is PARENT.
mergeParentNum is undefined for auto merge commits
+ Same as `parent` in CommentInfo.
*/
mergeParentNum?: number;
patchNum?: PatchSetNum;
+ /* Different from CommentInfo, which just keeps the line undefined for
+ FILE comments. */
line?: LineNumber;
- /* rootId is optional since we create a empty comment thread element for
- drafts and then create the draft which becomes the root */
- rootId?: UrlEncodedCommentId;
- diffSide?: Side;
range?: CommentRange;
- ported?: boolean; // is the comment ported over from a previous patchset
- rangeInfoLost?: boolean; // if BE was unable to determine a range for this
+ /**
+ * Was the thread ported over from its original location to a newer patchset?
+ * If yes, then the location information above contains the ported location,
+ * but the comments still have the original location set.
+ */
+ ported?: boolean;
+ /**
+ * Only relevant when ported:true. Means that no ported range could be
+ * computed. `line` and `range` can be undefined then.
+ */
+ rangeInfoLost?: boolean;
}
-export function getLastComment(thread?: CommentThread): UIComment | undefined {
- const len = thread?.comments.length;
- return thread && len ? thread.comments[len - 1] : undefined;
+export function equalLocation(t1?: CommentThread, t2?: CommentThread) {
+ if (t1 === t2) return true;
+ if (t1 === undefined || t2 === undefined) return false;
+ return (
+ t1.path === t2.path &&
+ t1.patchNum === t2.patchNum &&
+ t1.commentSide === t2.commentSide &&
+ t1.line === t2.line &&
+ t1.range?.start_line === t2.range?.start_line &&
+ t1.range?.start_character === t2.range?.start_character &&
+ t1.range?.end_line === t2.range?.end_line &&
+ t1.range?.end_character === t2.range?.end_character
+ );
}
-export function getFirstComment(thread?: CommentThread): UIComment | undefined {
- return thread?.comments?.[0];
+export function getLastComment(thread: CommentThread): CommentInfo | undefined {
+ const len = thread.comments.length;
+ return thread.comments[len - 1];
}
-export function countComments(thread?: CommentThread) {
- return thread?.comments?.length ?? 0;
+export function getLastPublishedComment(
+ thread: CommentThread
+): CommentInfo | undefined {
+ const publishedComments = thread.comments.filter(c => !isDraftOrUnsaved(c));
+ const len = publishedComments.length;
+ return publishedComments[len - 1];
}
-export function isPatchsetLevel(thread?: CommentThread): boolean {
- return thread?.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
+export function getFirstComment(
+ thread: CommentThread
+): CommentInfo | undefined {
+ return thread.comments[0];
}
-export function isUnresolved(thread?: CommentThread): boolean {
+export function countComments(thread: CommentThread) {
+ return thread.comments.length;
+}
+
+export function isPatchsetLevel(thread: CommentThread): boolean {
+ return thread.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
+}
+
+export function isUnresolved(thread: CommentThread): boolean {
return !isResolved(thread);
}
-export function isResolved(thread?: CommentThread): boolean {
- return !getLastComment(thread)?.unresolved;
+export function isResolved(thread: CommentThread): boolean {
+ const lastUnresolved = getLastComment(thread)?.unresolved;
+ return !lastUnresolved ?? false;
}
-export function isDraftThread(thread?: CommentThread): boolean {
+export function isDraftThread(thread: CommentThread): boolean {
return isDraft(getLastComment(thread));
}
-export function isRobotThread(thread?: CommentThread): boolean {
+export function isRobotThread(thread: CommentThread): boolean {
return isRobot(getFirstComment(thread));
}
-export function hasHumanReply(thread?: CommentThread): boolean {
+export function hasHumanReply(thread: CommentThread): boolean {
return countComments(thread) > 1 && !isRobot(getLastComment(thread));
}
+export function lastUpdated(thread: CommentThread): Date | undefined {
+ // We don't want to re-sort comments when you save a draft reply, so
+ // we stick to the timestampe of the last *published* comment.
+ const lastUpdated =
+ getLastPublishedComment(thread)?.updated ?? getLastComment(thread)?.updated;
+ return lastUpdated !== undefined ? parseDate(lastUpdated) : undefined;
+}
/**
* Whether the given comment should be included in the base side of the
* given patch range.
*/
export function isInBaseOfPatchRange(
- comment: CommentBasics,
+ comment: {
+ patch_set?: PatchSetNum;
+ side?: CommentSide;
+ parent?: number;
+ },
range: PatchRange
) {
// If the base of the patch range is a parent of a merge, and the comment
@@ -249,7 +331,10 @@
* given patch range.
*/
export function isInRevisionOfPatchRange(
- comment: CommentBasics,
+ comment: {
+ patch_set?: PatchSetNum;
+ side?: CommentSide;
+ },
range: PatchRange
) {
return (
@@ -271,7 +356,7 @@
}
export function getPatchRangeForCommentUrl(
- comment: UIComment,
+ comment: Comment,
latestPatchNum: RevisionPatchSetNum
) {
if (!comment.patch_set) throw new Error('Missing comment.patch_set');
@@ -279,7 +364,7 @@
// TODO(dhruvsri): Add handling for comment left on parents of merge commits
if (comment.side === CommentSide.PARENT) {
if (comment.patch_set === ParentPatchSetNum)
- throw new Error('diffSide cannot be PARENT');
+ throw new Error('comment.patch_set cannot be PARENT');
return {
patchNum: comment.patch_set as RevisionPatchSetNum,
basePatchNum: ParentPatchSetNum,
@@ -355,30 +440,46 @@
return authors;
}
-export function computeId(comment: UIComment) {
- if (comment.id) return comment.id;
- if (isDraft(comment)) return comment.__draftID;
- throw new Error('Missing id in root comment.');
-}
-
/**
- * Add path info to every comment as CommentInfo returned
- * from server does not have that.
- *
- * TODO(taoalpha): should consider changing BE to send path
- * back within CommentInfo
+ * Add path info to every comment as CommentInfo returned from server does not
+ * have that.
*/
export function addPath<T>(comments: {[path: string]: T[]} = {}): {
[path: string]: Array<T & {path: string}>;
} {
const updatedComments: {[path: string]: Array<T & {path: string}>} = {};
for (const filePath of Object.keys(comments)) {
- const allCommentsForPath = comments[filePath] || [];
- if (allCommentsForPath.length) {
- updatedComments[filePath] = allCommentsForPath.map(comment => {
- return {...comment, path: filePath};
- });
- }
+ updatedComments[filePath] = (comments[filePath] || []).map(comment => {
+ return {...comment, path: filePath};
+ });
}
return updatedComments;
}
+
+/**
+ * Add __draft:true to all drafts returned from server so that they can be told
+ * apart from published comments easily.
+ */
+export function addDraftProp(
+ draftsByPath: {[path: string]: CommentInfo[]} = {}
+) {
+ const updated: {[path: string]: DraftInfo[]} = {};
+ for (const filePath of Object.keys(draftsByPath)) {
+ updated[filePath] = (draftsByPath[filePath] ?? []).map(draft => {
+ return {...draft, __draft: true};
+ });
+ }
+ return updated;
+}
+
+export function reportingDetails(comment: CommentBasics) {
+ return {
+ id: comment?.id,
+ message_length: comment?.message?.trim().length,
+ in_reply_to: comment?.in_reply_to,
+ unresolved: comment?.unresolved,
+ path_length: comment?.path?.length,
+ line: comment?.range?.start_line ?? comment?.line,
+ unsaved: isUnsaved(comment),
+ };
+}
diff --git a/polygerrit-ui/app/utils/comment-util_test.ts b/polygerrit-ui/app/utils/comment-util_test.ts
index 3c8f26d..f5a2177 100644
--- a/polygerrit-ui/app/utils/comment-util_test.ts
+++ b/polygerrit-ui/app/utils/comment-util_test.ts
@@ -23,9 +23,8 @@
sortComments,
} from './comment-util';
import {createComment, createCommentThread} from '../test/test-data-generators';
-import {CommentSide, Side} from '../constants/constants';
+import {CommentSide} from '../constants/constants';
import {
- BasePatchSetNum,
ParentPatchSetNum,
PatchSetNum,
RevisionPatchSetNum,
@@ -37,7 +36,6 @@
test('isUnresolved', () => {
const thread = createCommentThread([createComment()]);
- assert.isFalse(isUnresolved(undefined));
assert.isFalse(isUnresolved(thread));
assert.isTrue(
@@ -97,7 +95,6 @@
{
id: 'new_draft' as UrlEncodedCommentId,
message: 'i do not like either of you',
- diffSide: Side.LEFT,
__draft: true,
updated: '2015-12-20 15:01:20.396000000' as Timestamp,
},
@@ -106,13 +103,11 @@
message: 'i like you, jack',
updated: '2015-12-23 15:00:20.396000000' as Timestamp,
line: 1,
- diffSide: Side.LEFT,
},
{
id: 'jacks_reply' as UrlEncodedCommentId,
message: 'i like you, too',
updated: '2015-12-24 15:01:20.396000000' as Timestamp,
- diffSide: Side.LEFT,
line: 1,
in_reply_to: 'sallys_confession',
},
@@ -153,21 +148,16 @@
},
];
- const actualThreads = createCommentThreads(comments, {
- basePatchNum: 1 as BasePatchSetNum,
- patchNum: 4 as RevisionPatchSetNum,
- });
+ const actualThreads = createCommentThreads(comments);
assert.equal(actualThreads.length, 2);
- assert.equal(actualThreads[0].diffSide, Side.LEFT);
assert.equal(actualThreads[0].comments.length, 2);
assert.deepEqual(actualThreads[0].comments[0], comments[0]);
assert.deepEqual(actualThreads[0].comments[1], comments[1]);
assert.equal(actualThreads[0].patchNum, 1 as PatchSetNum);
assert.equal(actualThreads[0].line, 1);
- assert.equal(actualThreads[1].diffSide, Side.LEFT);
assert.equal(actualThreads[1].comments.length, 1);
assert.deepEqual(actualThreads[1].comments[0], comments[2]);
assert.equal(actualThreads[1].patchNum, 1 as PatchSetNum);
@@ -194,7 +184,6 @@
const expectedThreads = [
{
- diffSide: Side.LEFT,
commentSide: CommentSide.REVISION,
path: '/p',
rootId: 'betsys_confession' as UrlEncodedCommentId,
@@ -226,13 +215,7 @@
},
];
- assert.deepEqual(
- createCommentThreads(comments, {
- basePatchNum: 5 as BasePatchSetNum,
- patchNum: 10 as RevisionPatchSetNum,
- }),
- expectedThreads
- );
+ assert.deepEqual(createCommentThreads(comments), expectedThreads);
});
test('does not thread unrelated comments at same location', () => {
@@ -241,14 +224,12 @@
id: 'sallys_confession' as UrlEncodedCommentId,
message: 'i like you, jack',
updated: '2015-12-23 15:00:20.396000000' as Timestamp,
- diffSide: Side.LEFT,
path: '/p',
},
{
id: 'jacks_reply' as UrlEncodedCommentId,
message: 'i like you, too',
updated: '2015-12-24 15:01:20.396000000' as Timestamp,
- diffSide: Side.LEFT,
path: '/p',
},
];
diff --git a/polygerrit-ui/app/utils/compare-util.ts b/polygerrit-ui/app/utils/compare-util.ts
deleted file mode 100644
index dd20915..0000000
--- a/polygerrit-ui/app/utils/compare-util.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * @license
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-export function deepEqualStringDict(
- a: {[name: string]: string},
- b: {[name: string]: string}
-): boolean {
- const aKeys = Object.keys(a);
- const bKeys = Object.keys(b);
- if (aKeys.length !== bKeys.length) return false;
- for (const key of aKeys) {
- if (a[key] !== b[key]) return false;
- }
- return true;
-}
-
-export function equalArray(a?: unknown[], b?: unknown[]): boolean {
- if (a === b) return true;
- if (a === undefined) return b === undefined;
- if (b === undefined) return a === undefined;
- if (a.length !== b.length) return false;
- for (let i = 0; i < a.length; i++) {
- if (a[i] !== b[i]) return false;
- }
- return true;
-}
diff --git a/polygerrit-ui/app/utils/compare-util_test.ts b/polygerrit-ui/app/utils/compare-util_test.ts
deleted file mode 100644
index 7cd71bf..0000000
--- a/polygerrit-ui/app/utils/compare-util_test.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * @license
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import '../test/common-test-setup-karma';
-import {deepEqualStringDict, equalArray} from './compare-util';
-
-suite('compare-utils tests', () => {
- test('deepEqual', () => {
- assert.isTrue(deepEqualStringDict({}, {}));
- assert.isTrue(deepEqualStringDict({x: 'y'}, {x: 'y'}));
- assert.isTrue(deepEqualStringDict({x: 'y', p: 'q'}, {p: 'q', x: 'y'}));
-
- assert.isFalse(deepEqualStringDict({}, {x: 'y'}));
- assert.isFalse(deepEqualStringDict({x: 'y'}, {x: 'z'}));
- assert.isFalse(deepEqualStringDict({x: 'y'}, {z: 'y'}));
- });
-
- test('equalArray', () => {
- assert.isTrue(equalArray(undefined, undefined));
- assert.isTrue(equalArray([], []));
- assert.isTrue(equalArray([1], [1]));
- assert.isTrue(equalArray(['a', 'b'], ['a', 'b']));
-
- assert.isFalse(equalArray(undefined, []));
- assert.isFalse(equalArray([], undefined));
- assert.isFalse(equalArray([], [1]));
- assert.isFalse(equalArray([1], [2]));
- assert.isFalse(equalArray([1, 2], [1]));
- });
-});
diff --git a/polygerrit-ui/app/utils/deep-util.ts b/polygerrit-ui/app/utils/deep-util.ts
new file mode 100644
index 0000000..694a8d7
--- /dev/null
+++ b/polygerrit-ui/app/utils/deep-util.ts
@@ -0,0 +1,50 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export function deepEqual<T>(a: T, b: T): boolean {
+ if (a === b) return true;
+ if (a === undefined || b === undefined) return false;
+ if (a === null || b === null) return false;
+ if (a instanceof Date && b instanceof Date)
+ return a.getTime() === b.getTime();
+
+ if (typeof a === 'object') {
+ if (typeof b !== 'object') return false;
+ const aObj = a as Record<string, unknown>;
+ const bObj = b as Record<string, unknown>;
+ const aKeys = Object.keys(aObj);
+ const bKeys = Object.keys(bObj);
+ if (aKeys.length !== bKeys.length) return false;
+ for (const key of aKeys) {
+ if (!deepEqual(aObj[key], bObj[key])) return false;
+ }
+ return true;
+ }
+
+ return false;
+}
+
+export function notDeepEqual<T>(a: T, b: T): boolean {
+ return !deepEqual(a, b);
+}
+
+/**
+ * @param obj Object
+ */
+export function deepClone(obj?: object) {
+ if (!obj) return undefined;
+ return JSON.parse(JSON.stringify(obj));
+}
diff --git a/polygerrit-ui/app/utils/deep-util_test.ts b/polygerrit-ui/app/utils/deep-util_test.ts
new file mode 100644
index 0000000..64c442b
--- /dev/null
+++ b/polygerrit-ui/app/utils/deep-util_test.ts
@@ -0,0 +1,71 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../test/common-test-setup-karma';
+import {deepEqual} from './deep-util';
+
+suite('compare-util tests', () => {
+ test('deepEqual primitives', () => {
+ assert.isTrue(deepEqual(undefined, undefined));
+ assert.isTrue(deepEqual(null, null));
+ assert.isTrue(deepEqual(0, 0));
+ assert.isTrue(deepEqual('', ''));
+
+ assert.isFalse(deepEqual(1, 2));
+ assert.isFalse(deepEqual('a', 'b'));
+ });
+
+ test('deepEqual Dates', () => {
+ const a = new Date();
+ const b = new Date(a.getTime());
+ assert.isTrue(deepEqual(a, b));
+ assert.isFalse(deepEqual(a, undefined));
+ assert.isFalse(deepEqual(undefined, b));
+ assert.isFalse(deepEqual(a, new Date(a.getTime() + 1)));
+ });
+
+ test('deepEqual objects', () => {
+ assert.isTrue(deepEqual({}, {}));
+ assert.isTrue(deepEqual({x: 'y'}, {x: 'y'}));
+ assert.isTrue(deepEqual({x: 'y', p: 'q'}, {p: 'q', x: 'y'}));
+ assert.isTrue(deepEqual({x: {y: 'y'}}, {x: {y: 'y'}}));
+
+ assert.isFalse(deepEqual(undefined, {}));
+ assert.isFalse(deepEqual(null, {}));
+ assert.isFalse(deepEqual({}, undefined));
+ assert.isFalse(deepEqual({}, null));
+ assert.isFalse(deepEqual({}, {x: 'y'}));
+ assert.isFalse(deepEqual({x: 'y'}, {x: 'z'}));
+ assert.isFalse(deepEqual({x: 'y'}, {z: 'y'}));
+ assert.isFalse(deepEqual({x: {y: 'y'}}, {x: {y: 'z'}}));
+ });
+
+ test('deepEqual arrays', () => {
+ assert.isTrue(deepEqual([], []));
+ assert.isTrue(deepEqual([1], [1]));
+ assert.isTrue(deepEqual(['a', 'b'], ['a', 'b']));
+ assert.isTrue(deepEqual(['a', ['b']], ['a', ['b']]));
+
+ assert.isFalse(deepEqual(undefined, []));
+ assert.isFalse(deepEqual(null, []));
+ assert.isFalse(deepEqual([], undefined));
+ assert.isFalse(deepEqual([], null));
+ assert.isFalse(deepEqual([], [1]));
+ assert.isFalse(deepEqual([1], [2]));
+ assert.isFalse(deepEqual([1, 2], [1]));
+ assert.isFalse(deepEqual(['a', ['b']], ['a', ['c']]));
+ });
+});
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index dd408d3..b96ebe6 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -338,6 +338,8 @@
combo?: ComboKey;
/** Defaults to no modifiers. */
modifiers?: Modifier[];
+ /** Defaults to false. If true, then `event.repeat === true` is allowed. */
+ allowRepeat?: boolean;
}
const ALPHA_NUM = new RegExp(/^[A-Za-z0-9]$/);
@@ -388,29 +390,51 @@
return true;
}
-export function addGlobalShortcut(
- shortcut: Binding,
- listener: (e: KeyboardEvent) => void
-) {
- return addShortcut(document.body, shortcut, listener);
+export interface ShortcutOptions {
+ /**
+ * Do you want to suppress events from <input> elements and such?
+ */
+ shouldSuppress?: boolean;
+ /**
+ * Do you want to take care of calling preventDefault() and
+ * stopPropagation() yourself?
+ */
+ doNotPrevent?: boolean;
}
+export function addGlobalShortcut(
+ shortcut: Binding,
+ listener: (e: KeyboardEvent) => void,
+ options: ShortcutOptions = {
+ shouldSuppress: true,
+ doNotPrevent: false,
+ }
+) {
+ return addShortcut(document.body, shortcut, listener, options);
+}
+
+/**
+ * Deprecated.
+ *
+ * For LitElement use the shortcut-controller.
+ * For PolymerElement use the keyboard-shortcut-mixin.
+ */
export function addShortcut(
element: HTMLElement,
shortcut: Binding,
listener: (e: KeyboardEvent) => void,
- options: {
- shouldSuppress: boolean;
- } = {
+ options: ShortcutOptions = {
shouldSuppress: false,
+ doNotPrevent: false,
}
) {
const wrappedListener = (e: KeyboardEvent) => {
- if (e.repeat) return;
+ if (e.repeat && !shortcut.allowRepeat) return;
if (options.shouldSuppress && shouldSuppress(e)) return;
- if (eventMatchesShortcut(e, shortcut)) {
- listener(e);
- }
+ if (!eventMatchesShortcut(e, shortcut)) return;
+ if (!options.doNotPrevent) e.preventDefault();
+ if (!options.doNotPrevent) e.stopPropagation();
+ listener(e);
};
element.addEventListener('keydown', wrappedListener);
return () => element.removeEventListener('keydown', wrappedListener);
diff --git a/polygerrit-ui/app/utils/dom-util_test.ts b/polygerrit-ui/app/utils/dom-util_test.ts
index 5429550..28157d9 100644
--- a/polygerrit-ui/app/utils/dom-util_test.ts
+++ b/polygerrit-ui/app/utils/dom-util_test.ts
@@ -28,22 +28,28 @@
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {html} from '@polymer/polymer/lib/utils/html-tag';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
-import {queryAndAssert} from '../test/test-utils';
+import {mockPromise, queryAndAssert} from '../test/test-utils';
-async function keyEventOn(
+/**
+ * You might think that instead of passing in the callback with assertions as a
+ * parameter that you could as well just `await keyEventOn()` and *then* run
+ * your assertions. But at that point the event is not "hot" anymore, so most
+ * likely you want to assert stuff about the event within the callback
+ * parameter.
+ */
+function keyEventOn(
el: HTMLElement,
callback: (e: KeyboardEvent) => void,
keyCode = 75,
key = 'k'
): Promise<KeyboardEvent> {
- let resolve: (e: KeyboardEvent) => void;
- const promise = new Promise<KeyboardEvent>(r => (resolve = r));
+ const promise = mockPromise<KeyboardEvent>();
el.addEventListener('keydown', (e: KeyboardEvent) => {
callback(e);
- resolve(e);
+ promise.resolve(e);
});
MockInteractions.keyDownOn(el, keyCode, null, key);
- return await promise;
+ return promise;
}
class TestEle extends PolymerElement {
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index c50bbe2..437258c 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -15,10 +15,12 @@
* limitations under the License.
*/
import {
+ ChangeInfo,
isQuickLabelInfo,
SubmitRequirementResultInfo,
SubmitRequirementStatus,
} from '../api/rest-api';
+import {FlagsService, KnownExperimentId} from '../services/flags/flags';
import {
AccountInfo,
ApprovalInfo,
@@ -28,6 +30,7 @@
LabelNameToInfoMap,
VotingRangeInfo,
} from '../types/common';
+import {ParsedChangeInfo} from '../types/types';
import {assertNever, unique} from './common-util';
// Name of the standard Code-Review label.
@@ -88,8 +91,10 @@
: LabelStatus.RECOMMENDED;
}
} else if (isQuickLabelInfo(label)) {
- if (label.approved) return LabelStatus.RECOMMENDED;
- if (label.rejected) return LabelStatus.DISLIKED;
+ if (label.approved) return LabelStatus.APPROVED;
+ if (label.rejected) return LabelStatus.REJECTED;
+ if (label.disliked) return LabelStatus.DISLIKED;
+ if (label.recommended) return LabelStatus.RECOMMENDED;
}
return LabelStatus.NEUTRAL;
}
@@ -176,7 +181,12 @@
);
}
if (isQuickLabelInfo(labelInfo)) {
- return !!labelInfo.rejected || !!labelInfo.approved;
+ return (
+ !!labelInfo.rejected ||
+ !!labelInfo.approved ||
+ !!labelInfo.recommended ||
+ !!labelInfo.disliked
+ );
}
return false;
}
@@ -204,39 +214,64 @@
return;
}
-export function extractAssociatedLabels(
- requirement: SubmitRequirementResultInfo
-): string[] {
+function extractLabelsFrom(expression: string) {
const pattern = new RegExp('label[0-9]*:([\\w-]+)', 'g');
const labels = [];
let match;
- while (
- (match = pattern.exec(
- requirement.submittability_expression_result.expression
- )) !== null
- ) {
+ while ((match = pattern.exec(expression)) !== null) {
labels.push(match[1]);
}
+ return labels;
+}
+
+export function extractAssociatedLabels(
+ requirement: SubmitRequirementResultInfo,
+ type: 'all' | 'onlyOverride' | 'onlySubmittability' = 'all'
+): string[] {
+ let labels: string[] = [];
+ if (type !== 'onlyOverride') {
+ labels = labels.concat(
+ extractLabelsFrom(requirement.submittability_expression_result.expression)
+ );
+ }
+ if (requirement.override_expression_result && type !== 'onlySubmittability') {
+ labels = labels.concat(
+ extractLabelsFrom(requirement.override_expression_result.expression)
+ );
+ }
return labels.filter(unique);
}
export function iconForStatus(status: SubmitRequirementStatus) {
switch (status) {
case SubmitRequirementStatus.SATISFIED:
- return 'check';
+ return 'check-circle-filled';
case SubmitRequirementStatus.UNSATISFIED:
- return 'close';
+ return 'block';
case SubmitRequirementStatus.OVERRIDDEN:
return 'overridden';
case SubmitRequirementStatus.NOT_APPLICABLE:
return 'info';
+ case SubmitRequirementStatus.ERROR:
+ return 'error';
+ case SubmitRequirementStatus.FORCED:
+ return 'check-circle-filled';
default:
assertNever(status, `Unsupported status: ${status}`);
}
}
+/**
+ * Show only applicable.
+ */
+export function getRequirements(change?: ParsedChangeInfo | ChangeInfo) {
+ return (change?.submit_requirements ?? []).filter(
+ req => req.status !== SubmitRequirementStatus.NOT_APPLICABLE
+ );
+}
+
// TODO(milutin): This may be temporary for demo purposes
-const PRIORITY_REQUIREMENTS_ORDER: string[] = [
+export const PRIORITY_REQUIREMENTS_ORDER: string[] = [
StandardLabels.CODE_REVIEW,
StandardLabels.CODE_OWNERS,
StandardLabels.PRESUBMIT_VERIFIED,
@@ -255,3 +290,27 @@
);
return priorityRequirementList.concat(nonPriorityRequirements);
}
+
+export function getTriggerVotes(change?: ParsedChangeInfo | ChangeInfo) {
+ const allLabels = Object.keys(change?.labels ?? {});
+ const submitReqs = getRequirements(change);
+ const labelAssociatedWithSubmitReqs = submitReqs
+ .flatMap(req => extractAssociatedLabels(req))
+ .filter(unique);
+ return allLabels.filter(
+ label => !labelAssociatedWithSubmitReqs.includes(label)
+ );
+}
+
+export function showNewSubmitRequirements(
+ flagsService: FlagsService,
+ change?: ParsedChangeInfo | ChangeInfo
+) {
+ const isSubmitRequirementsUiEnabled = flagsService.isEnabled(
+ KnownExperimentId.SUBMIT_REQUIREMENTS_UI
+ );
+ if (!isSubmitRequirementsUiEnabled) return false;
+ if ((getRequirements(change) ?? []).length === 0) return false;
+
+ return true;
+}
diff --git a/polygerrit-ui/app/utils/label-util_test.ts b/polygerrit-ui/app/utils/label-util_test.ts
index 9360688..fbd0aa1 100644
--- a/polygerrit-ui/app/utils/label-util_test.ts
+++ b/polygerrit-ui/app/utils/label-util_test.ts
@@ -24,6 +24,8 @@
getRepresentativeValue,
getVotingRange,
getVotingRangeOrDefault,
+ getRequirements,
+ getTriggerVotes,
hasNeutralStatus,
labelCompare,
LabelStatus,
@@ -38,9 +40,15 @@
} from '../types/common';
import {
createAccountWithEmail,
+ createChange,
createSubmitRequirementExpressionInfo,
createSubmitRequirementResultInfo,
+ createDetailedLabelInfo,
} from '../test/test-data-generators';
+import {
+ SubmitRequirementResultInfo,
+ SubmitRequirementStatus,
+} from '../api/rest-api';
const VALUES_0 = {
'0': 'neutral',
@@ -179,8 +187,12 @@
let labelInfo: QuickLabelInfo = {};
assert.equal(getLabelStatus(labelInfo), LabelStatus.NEUTRAL);
labelInfo = {approved: createAccountWithEmail()};
- assert.equal(getLabelStatus(labelInfo), LabelStatus.RECOMMENDED);
+ assert.equal(getLabelStatus(labelInfo), LabelStatus.APPROVED);
labelInfo = {rejected: createAccountWithEmail()};
+ assert.equal(getLabelStatus(labelInfo), LabelStatus.REJECTED);
+ labelInfo = {recommended: createAccountWithEmail()};
+ assert.equal(getLabelStatus(labelInfo), LabelStatus.RECOMMENDED);
+ labelInfo = {disliked: createAccountWithEmail()};
assert.equal(getLabelStatus(labelInfo), LabelStatus.DISLIKED);
});
@@ -256,5 +268,91 @@
const labels = extractAssociatedLabels(submitRequirement);
assert.deepEqual(labels, ['Verified', 'Code-Review']);
});
+ test('overridden label', () => {
+ const submitRequirement = {
+ ...createSubmitRequirementExpressionInfoWith(
+ 'label:Verified=MAX -label:Verified=MIN'
+ ),
+ override_expression_result: {
+ ...createSubmitRequirementExpressionInfo(),
+ expression: 'label:Build-cop-override',
+ },
+ };
+ const labels = extractAssociatedLabels(submitRequirement);
+ assert.deepEqual(labels, ['Verified', 'Build-cop-override']);
+ });
+ });
+
+ suite('getRequirements()', () => {
+ function createChangeInfoWith(
+ submit_requirements: SubmitRequirementResultInfo[]
+ ) {
+ return {
+ ...createChange(),
+ submit_requirements,
+ };
+ }
+ test('only legacy', () => {
+ const requirement = {
+ ...createSubmitRequirementResultInfo(),
+ is_legacy: true,
+ };
+ const change = createChangeInfoWith([requirement]);
+ assert.deepEqual(getRequirements(change), [requirement]);
+ });
+ test('legacy and non-legacy - show all', () => {
+ const requirement = {
+ ...createSubmitRequirementResultInfo(),
+ is_legacy: true,
+ };
+ const requirement2 = {
+ ...createSubmitRequirementResultInfo(),
+ is_legacy: false,
+ };
+ const change = createChangeInfoWith([requirement, requirement2]);
+ assert.deepEqual(getRequirements(change), [requirement, requirement2]);
+ });
+ test('filter not applicable', () => {
+ const requirement = createSubmitRequirementResultInfo();
+ const requirement2 = {
+ ...createSubmitRequirementResultInfo(),
+ status: SubmitRequirementStatus.NOT_APPLICABLE,
+ };
+ const change = createChangeInfoWith([requirement, requirement2]);
+ assert.deepEqual(getRequirements(change), [requirement]);
+ });
+ });
+
+ suite('getTriggerVotes()', () => {
+ test('no requirements', () => {
+ const triggerVote = 'Trigger-Vote';
+ const change = {
+ ...createChange(),
+ labels: {
+ [triggerVote]: createDetailedLabelInfo(),
+ },
+ };
+ assert.deepEqual(getTriggerVotes(change), [triggerVote]);
+ });
+ test('no trigger votes, all labels associated with sub requirement', () => {
+ const triggerVote = 'Trigger-Vote';
+ const change = {
+ ...createChange(),
+ submit_requirements: [
+ {
+ ...createSubmitRequirementResultInfo(),
+ submittability_expression_result: {
+ ...createSubmitRequirementExpressionInfo(),
+ expression: `label:${triggerVote}=MAX`,
+ },
+ is_legacy: false,
+ },
+ ],
+ labels: {
+ [triggerVote]: createDetailedLabelInfo(),
+ },
+ };
+ assert.deepEqual(getTriggerVotes(change), []);
+ });
});
});
diff --git a/polygerrit-ui/app/elements/gr-app_html.ts b/polygerrit-ui/app/utils/math-util.ts
similarity index 63%
copy from polygerrit-ui/app/elements/gr-app_html.ts
copy to polygerrit-ui/app/utils/math-util.ts
index f6172c9..adec7d3 100644
--- a/polygerrit-ui/app/elements/gr-app_html.ts
+++ b/polygerrit-ui/app/utils/math-util.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-export const htmlTemplate = html`
- <gr-app-element id="app-element"></gr-app-element>
-`;
+/**
+ * Returns a random integer between `from` and `to`, both included.
+ * So getRandomInt(0, 2) returns 0, 1, or 2 each with probability 1/3.
+ */
+export function getRandomInt(from: number, to: number) {
+ return Math.floor(Math.random() * (to + 1 - from) + from);
+}
diff --git a/polygerrit-ui/app/utils/math-util_test.ts b/polygerrit-ui/app/utils/math-util_test.ts
new file mode 100644
index 0000000..fca1d73
--- /dev/null
+++ b/polygerrit-ui/app/utils/math-util_test.ts
@@ -0,0 +1,63 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../test/common-test-setup-karma';
+import {getRandomInt} from './math-util';
+
+suite('math-util tests', () => {
+ test('getRandomInt', () => {
+ let r = 0;
+ const randomStub = sinon.stub(Math, 'random').callsFake(() => r);
+
+ assert.equal(getRandomInt(0, 0), 0);
+ assert.equal(getRandomInt(0, 2), 0);
+ assert.equal(getRandomInt(0, 100), 0);
+ assert.equal(getRandomInt(10, 10), 10);
+ assert.equal(getRandomInt(10, 12), 10);
+ assert.equal(getRandomInt(10, 100), 10);
+
+ r = 0.999;
+ assert.equal(getRandomInt(0, 0), 0);
+ assert.equal(getRandomInt(0, 2), 2);
+ assert.equal(getRandomInt(0, 100), 100);
+ assert.equal(getRandomInt(10, 10), 10);
+ assert.equal(getRandomInt(10, 12), 12);
+ assert.equal(getRandomInt(10, 100), 100);
+
+ r = 0.5;
+ assert.equal(getRandomInt(0, 0), 0);
+ assert.equal(getRandomInt(0, 2), 1);
+ assert.equal(getRandomInt(0, 100), 50);
+ assert.equal(getRandomInt(10, 10), 10);
+ assert.equal(getRandomInt(10, 12), 11);
+ assert.equal(getRandomInt(10, 100), 55);
+
+ r = 0.0;
+ assert.equal(getRandomInt(0, 2), 0);
+ r = 0.33;
+ assert.equal(getRandomInt(0, 2), 0);
+ r = 0.34;
+ assert.equal(getRandomInt(0, 2), 1);
+ r = 0.66;
+ assert.equal(getRandomInt(0, 2), 1);
+ r = 0.67;
+ assert.equal(getRandomInt(0, 2), 2);
+ r = 0.99;
+ assert.equal(getRandomInt(0, 2), 2);
+
+ randomStub.restore();
+ });
+});
diff --git a/polygerrit-ui/app/elements/gr-app_html.ts b/polygerrit-ui/app/utils/observable-util.ts
similarity index 60%
copy from polygerrit-ui/app/elements/gr-app_html.ts
copy to polygerrit-ui/app/utils/observable-util.ts
index f6172c9..e39aa48 100644
--- a/polygerrit-ui/app/elements/gr-app_html.ts
+++ b/polygerrit-ui/app/utils/observable-util.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright (C) 2020 The Android Open Source Project
+ * Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {html} from '@polymer/polymer/lib/utils/html-tag';
+import {Observable} from 'rxjs';
+import {distinctUntilChanged, map, shareReplay} from 'rxjs/operators';
+import {deepEqual} from './deep-util';
-export const htmlTemplate = html`
- <gr-app-element id="app-element"></gr-app-element>
-`;
+export function select<A, B>(obs$: Observable<A>, mapper: (_: A) => B) {
+ return obs$.pipe(
+ map(mapper),
+ distinctUntilChanged(deepEqual),
+ shareReplay(1)
+ );
+}
diff --git a/polygerrit-ui/app/utils/patch-set-util.ts b/polygerrit-ui/app/utils/patch-set-util.ts
index ce5e5a4..cccb8ec 100644
--- a/polygerrit-ui/app/utils/patch-set-util.ts
+++ b/polygerrit-ui/app/utils/patch-set-util.ts
@@ -95,7 +95,7 @@
* @return The correspondent revision obj from {revisions}
*/
export function getRevisionByPatchNum(
- revisions: RevisionInfo[],
+ revisions: (RevisionInfo | EditRevisionInfo)[],
patchNum: PatchSetNum
) {
for (const rev of revisions) {
@@ -103,7 +103,7 @@
return rev;
}
}
- console.warn('no revision found');
+ if (revisions.length > 0) console.warn('no revision found');
return;
}
@@ -118,20 +118,26 @@
}
/**
+ * Find change edit revision if change edit exists.
+ */
+export function findEdit(
+ revisions: Array<RevisionInfo | EditRevisionInfo>
+): EditRevisionInfo | undefined {
+ const editRev = revisions.find(info => info._number === EditPatchSetNum);
+ return editRev as EditRevisionInfo | undefined;
+}
+
+/**
* Find change edit base revision if change edit exists.
*
* @return change edit parent revision or null if change edit
* doesn't exist.
- *
*/
export function findEditParentRevision(
revisions: Array<RevisionInfo | EditRevisionInfo>
) {
- const editInfo = revisions.find(info => info._number === EditPatchSetNum);
-
- if (!editInfo) {
- return null;
- }
+ const editInfo = findEdit(revisions);
+ if (!editInfo) return null;
return revisions.find(info => info._number === editInfo.basePatchNum) || null;
}
@@ -309,10 +315,11 @@
*/
export function findSortedIndex(
patchNum: PatchSetNum,
- revisions: RevisionInfo[]
+ revisions: (RevisionInfo | EditRevisionInfo)[]
) {
revisions = revisions || [];
- const findNum = (rev: RevisionInfo) => `${rev._number}` === `${patchNum}`;
+ const findNum = (rev: RevisionInfo | EditRevisionInfo) =>
+ `${rev._number}` === `${patchNum}`;
return revisions.findIndex(findNum);
}
diff --git a/polygerrit-ui/app/utils/string-util.ts b/polygerrit-ui/app/utils/string-util.ts
index 43c0765..0b217ec 100644
--- a/polygerrit-ui/app/utils/string-util.ts
+++ b/polygerrit-ui/app/utils/string-util.ts
@@ -38,3 +38,12 @@
if (n % 10 === 3 && n % 100 !== 13) return `${n}rd`;
return `${n}th`;
}
+
+/**
+ * This converts any inputed value into string.
+ *
+ * This is so typescript checker doesn't fail.
+ */
+export function convertToString(key?: unknown) {
+ return key !== undefined ? String(key) : '';
+}
diff --git a/polygerrit-ui/app/utils/url-util_test.ts b/polygerrit-ui/app/utils/url-util_test.ts
index 63dc81d..1a8b536 100644
--- a/polygerrit-ui/app/utils/url-util_test.ts
+++ b/polygerrit-ui/app/utils/url-util_test.ts
@@ -28,10 +28,11 @@
toPathname,
toSearchParams,
} from './url-util';
-import {appContext} from '../services/app-context';
+import {getAppContext, AppContext} from '../services/app-context';
import {stubRestApi} from '../test/test-utils';
suite('url-util tests', () => {
+ let appContext: AppContext;
suite('getBaseUrl tests', () => {
let originalCanonicalPath: string | undefined;
@@ -52,6 +53,7 @@
suite('getDocsBaseUrl tests', () => {
setup(() => {
_testOnly_clearDocsBaseUrlCache();
+ appContext = getAppContext();
});
test('null config', async () => {
diff --git a/polygerrit-ui/karma.conf.js b/polygerrit-ui/karma.conf.js
index a3b694f..d656eb7 100644
--- a/polygerrit-ui/karma.conf.js
+++ b/polygerrit-ui/karma.conf.js
@@ -111,6 +111,8 @@
...additionalFiles,
getUiDevNpmFilePath('source-map-support/browser-source-map-support.js'),
getUiDevNpmFilePath('accessibility-developer-tools/dist/js/axs_testing.js'),
+ {pattern: getUiDevNpmFilePath('@open-wc/semantic-dom-diff/index.js'), type: 'module' },
+ {pattern: getUiDevNpmFilePath('@open-wc/testing-helpers/index.js'), type: 'module' },
getUiDevNpmFilePath('sinon/pkg/sinon.js'),
{ pattern: testFilesPattern, type: 'module' },
],
diff --git a/polygerrit-ui/package.json b/polygerrit-ui/package.json
index 793703e..77d93c3 100644
--- a/polygerrit-ui/package.json
+++ b/polygerrit-ui/package.json
@@ -10,11 +10,13 @@
},
"devDependencies": {
"@open-wc/karma-esm": "^2.16.16",
+ "@open-wc/semantic-dom-diff": "^0.19.5",
+ "@open-wc/testing-helpers": "^2.0.2",
"@polymer/iron-test-helpers": "^3.0.1",
"@polymer/test-fixture": "^4.0.2",
"accessibility-developer-tools": "^2.12.0",
"chai": "^4.3.4",
- "karma": "^6.3.4",
+ "karma": "^6.3.6",
"karma-chrome-launcher": "^3.1.0",
"karma-mocha": "^2.0.1",
"karma-mocha-reporter": "^2.2.5",
diff --git a/polygerrit-ui/yarn.lock b/polygerrit-ui/yarn.lock
index 7c7ef45..c148f3e 100644
--- a/polygerrit-ui/yarn.lock
+++ b/polygerrit-ui/yarn.lock
@@ -2,6 +2,13 @@
# yarn lockfile v1
+"@babel/code-frame@^7.12.11":
+ version "7.16.0"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.0.tgz#0dfc80309beec8411e65e706461c408b0bb9b431"
+ integrity sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA==
+ dependencies:
+ "@babel/highlight" "^7.16.0"
+
"@babel/code-frame@^7.14.5":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb"
@@ -218,6 +225,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz#6654d171b2024f6d8ee151bf2509699919131d48"
integrity sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==
+"@babel/helper-validator-identifier@^7.15.7":
+ version "7.15.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389"
+ integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==
+
"@babel/helper-validator-option@^7.14.5":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3"
@@ -251,6 +263,15 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
+"@babel/highlight@^7.16.0":
+ version "7.16.0"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.0.tgz#6ceb32b2ca4b8f5f361fb7fd821e3fddf4a1725a"
+ integrity sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.15.7"
+ chalk "^2.0.0"
+ js-tokens "^4.0.0"
+
"@babel/parser@^7.1.0", "@babel/parser@^7.14.5", "@babel/parser@^7.15.0", "@babel/parser@^7.4.3":
version "7.15.3"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.3.tgz#3416d9bea748052cfcb63dbcc27368105b1ed862"
@@ -882,6 +903,32 @@
dependencies:
vary "^1.1.2"
+"@lit/reactive-element@^1.0.0":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.0.2.tgz#daa7a7c7a6c63d735f0c9634de6b7dbd70a702ab"
+ integrity sha512-oz3d3MKjQ2tXynQgyaQaMpGTDNyNDeBdo6dXf1AbjTwhA1IRINHmA7kSaVYv9ttKweNkEoNqp9DqteDdgWzPEg==
+
+"@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"
+ integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
+ dependencies:
+ "@nodelib/fs.stat" "2.0.5"
+ run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
+ integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+
+"@nodelib/fs.walk@^1.2.3":
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
+ integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
+ dependencies:
+ "@nodelib/fs.scandir" "2.1.5"
+ fastq "^1.6.0"
+
"@open-wc/building-utils@^2.18.0", "@open-wc/building-utils@^2.18.3":
version "2.18.4"
resolved "https://registry.yarnpkg.com/@open-wc/building-utils/-/building-utils-2.18.4.tgz#397e42039f5d26c38f7a2cc01e347e0e5c2e8e99"
@@ -914,6 +961,11 @@
whatwg-fetch "^3.5.0"
whatwg-url "^7.1.0"
+"@open-wc/dedupe-mixin@^1.3.0":
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/@open-wc/dedupe-mixin/-/dedupe-mixin-1.3.0.tgz#0df5d438285fc3482838786ee81895318f0ff778"
+ integrity sha512-UfdK1MPnR6T7f3svzzYBfu3qBkkZ/KsPhcpc3JYhsUY4hbpwNF9wEQtD4Z+/mRqMTJrKg++YSxIxE0FBhY3RIw==
+
"@open-wc/karma-esm@^2.16.16":
version "2.16.18"
resolved "https://registry.yarnpkg.com/@open-wc/karma-esm/-/karma-esm-2.16.18.tgz#01f3f8c694d7b8dd3aef3159659f3fa9d3090c44"
@@ -930,6 +982,31 @@
portfinder "^1.0.21"
request "^2.88.0"
+"@open-wc/scoped-elements@^2.0.1":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@open-wc/scoped-elements/-/scoped-elements-2.0.1.tgz#6b1c3535f809bd90710574db80093a81e3a1fc2d"
+ integrity sha512-JS6ozxUFwFX3+Er91v9yQzNIaFn7OnE0iESKTbFvkkKdNwvAPtp1fpckBKIvWk8Ae9ZcoI9DYZuT2DDbMPcadA==
+ dependencies:
+ "@lit/reactive-element" "^1.0.0"
+ "@open-wc/dedupe-mixin" "^1.3.0"
+ "@webcomponents/scoped-custom-element-registry" "^0.0.3"
+
+"@open-wc/semantic-dom-diff@^0.19.5":
+ version "0.19.5"
+ resolved "https://registry.yarnpkg.com/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.19.5.tgz#8d3d7f69140b9ba477a4adf8099c79e0efe18955"
+ integrity sha512-Wi0Fuj3dzqlWClU0y+J4k/nqTcH0uwgOWxZXPyeyG3DdvuyyjgiT4L4I/s6iVShWQvvEsyXnj7yVvixAo3CZvg==
+ dependencies:
+ "@types/chai" "^4.2.11"
+ "@web/test-runner-commands" "^0.5.7"
+
+"@open-wc/testing-helpers@^2.0.2":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@open-wc/testing-helpers/-/testing-helpers-2.0.2.tgz#ca1833bf76036d9bdc03547415e79b6d502c78f6"
+ integrity sha512-wJlvDmWo+fIbgykRP21YSP9I9Pf/fo2+dZGaWG77Hw0sIuyB+7sNUDJDkL6kMkyyRecPV6dVRmbLt6HuOwvZ1w==
+ dependencies:
+ "@open-wc/scoped-elements" "^2.0.1"
+ lit "^2.0.0"
+
"@polymer/iron-test-helpers@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@polymer/iron-test-helpers/-/iron-test-helpers-3.0.1.tgz#ec2b9c6567e2967a191b3d800a04b1167b2d1394"
@@ -1004,6 +1081,11 @@
dependencies:
"@types/node" "*"
+"@types/babel__code-frame@^7.0.2":
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/@types/babel__code-frame/-/babel__code-frame-7.0.3.tgz#eda94e1b7c9326700a4b69c485ebbc9498a0b63f"
+ integrity sha512-2TN6oiwtNjOezilFVl77zwdNPwQWaDBBCCWWxyo1ctiO3vAtd7H/aB/CBJdw9+kqq3+latD0SXoedIuHySSZWw==
+
"@types/babel__core@^7.1.3":
version "7.1.15"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.15.tgz#2ccfb1ad55a02c83f8e0ad327cbc332f55eb1024"
@@ -1062,11 +1144,24 @@
resolved "https://registry.yarnpkg.com/@types/caniuse-api/-/caniuse-api-3.0.2.tgz#684ba0c284b2a58346abf0000bd0a735ad072d75"
integrity sha512-YfCDMn7R59n7GFFfwjPAM0zLJQy4UvveC32rOJBmTqJJY8uSRqM4Dc7IJj8V9unA48Qy4nj5Bj3jD6Q8VZ1Seg==
+"@types/chai@^4.2.11":
+ version "4.2.22"
+ resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.22.tgz#47020d7e4cf19194d43b5202f35f75bd2ad35ce7"
+ integrity sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ==
+
"@types/chai@^4.2.16":
version "4.2.21"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.21.tgz#9f35a5643129df132cf3b5c1ec64046ea1af0650"
integrity sha512-yd+9qKmJxm496BOV9CMNaey8TWsikaZOwMRwPHQIjcOJM9oV+fi9ZMNw3JsVnbEEbo2gRTDnGEBv8pjyn67hNg==
+"@types/co-body@^6.1.0":
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/@types/co-body/-/co-body-6.1.0.tgz#b52625390eb0d113c9b697ea92c3ffae7740cdb9"
+ integrity sha512-3e0q2jyDAnx/DSZi0z2H0yoZ2wt5yRDZ+P7ymcMObvq0ufWRT4tsajyO+Q1VwVWiv9PRR4W3YEjEzBjeZlhF+w==
+ dependencies:
+ "@types/node" "*"
+ "@types/qs" "*"
+
"@types/command-line-args@^5.0.0":
version "5.2.0"
resolved "https://registry.yarnpkg.com/@types/command-line-args/-/command-line-args-5.2.0.tgz#adbb77980a1cc376bb208e3f4142e907410430f6"
@@ -1094,7 +1189,12 @@
resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.4.tgz#de48cf01c79c9f1560bcfd8ae43217ab028657f8"
integrity sha512-0mPF08jn9zYI0n0Q/Pnz7C4kThdSt+6LD4amsrYDDpgBfrVWa3TcCOxKX1zkGgYniGagRv8heN2cbh+CAn+uuQ==
-"@types/cookie@^0.4.0":
+"@types/convert-source-map@^1.5.1":
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/@types/convert-source-map/-/convert-source-map-1.5.2.tgz#318dc22d476632a4855594c16970c6dc3ed086e7"
+ integrity sha512-tHs++ZeXer40kCF2JpE51Hg7t4HPa18B1b1Dzy96S0eCw8QKECNMYMfwa1edK/x8yCN0r4e6ewvLcc5CsVGkdg==
+
+"@types/cookie@^0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d"
integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==
@@ -1109,7 +1209,7 @@
"@types/keygrip" "*"
"@types/node" "*"
-"@types/cors@^2.8.8":
+"@types/cors@^2.8.12":
version "2.8.12"
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080"
integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==
@@ -1160,6 +1260,25 @@
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.1.tgz#e81ad28a60bee0328c6d2384e029aec626f1ae67"
integrity sha512-e+2rjEwK6KDaNOm5Aa9wNGgyS9oSZU/4pfSMMPYNOfjvFI0WVXm29+ITRFr6aKDvvKo7uU1jV68MW4ScsfDi7Q==
+"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.3":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
+ integrity sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==
+
+"@types/istanbul-lib-report@*":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686"
+ integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==
+ dependencies:
+ "@types/istanbul-lib-coverage" "*"
+
+"@types/istanbul-reports@^3.0.0":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff"
+ integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==
+ dependencies:
+ "@types/istanbul-lib-report" "*"
+
"@types/keygrip@*":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72"
@@ -1203,7 +1322,7 @@
"@types/koa" "*"
"@types/koa-send" "*"
-"@types/koa@*", "@types/koa@^2.0.48":
+"@types/koa@*", "@types/koa@^2.0.48", "@types/koa@^2.11.6":
version "2.13.4"
resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.4.tgz#10620b3f24a8027ef5cbae88b393d1b31205726b"
integrity sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw==
@@ -1259,6 +1378,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.1.tgz#aee62c7b966f55fc66c7b6dfa1d58db2a616da61"
integrity sha512-Sr7BhXEAer9xyGuCN3Ek9eg9xPviCF2gfu9kTfuU2HkTVAMYSDeX40fvpmo72n5nansg3nsBjuQBrsS28r+NUw==
+"@types/parse5@^6.0.1":
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.2.tgz#99f6b72d82e34cea03a4d8f2ed72114d909c1c61"
+ integrity sha512-+hQX+WyJAOne7Fh3zF5CxPemILIbuhNcqHHodzK9caYOLnC8pD5efmPleRnw0z++LfKUC/sVNMwk0Gap+B0baA==
+
"@types/path-is-inside@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/path-is-inside/-/path-is-inside-1.0.0.tgz#02d6ff38975d684bdec96204494baf9f29f0e17f"
@@ -1296,6 +1420,11 @@
dependencies:
"@sinonjs/fake-timers" "^7.1.0"
+"@types/trusted-types@^2.0.2":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756"
+ integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==
+
"@types/whatwg-url@^6.4.0":
version "6.4.0"
resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-6.4.0.tgz#1e59b8c64bc0dbdf66d037cf8449d1c3d5270237"
@@ -1303,11 +1432,102 @@
dependencies:
"@types/node" "*"
+"@types/ws@^7.4.0":
+ version "7.4.7"
+ resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702"
+ integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==
+ dependencies:
+ "@types/node" "*"
+
"@ungap/promise-all-settled@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
+"@web/browser-logs@^0.2.1":
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/@web/browser-logs/-/browser-logs-0.2.5.tgz#0895efb641eacb0fbc1138c6092bd18c01df2734"
+ integrity sha512-Qxo1wY/L7yILQqg0jjAaueh+tzdORXnZtxQgWH23SsTCunz9iq9FvsZa8Q5XlpjnZ3vLIsFEuEsCMqFeohJnEg==
+ dependencies:
+ errorstacks "^2.2.0"
+
+"@web/dev-server-core@^0.3.16":
+ version "0.3.17"
+ resolved "https://registry.yarnpkg.com/@web/dev-server-core/-/dev-server-core-0.3.17.tgz#95e87681b63644a955e29e13ffc6b48fd2c51264"
+ integrity sha512-vN1dwQ8yDHGiAvCeUo9xFfjo+pFl8TW+pON7k9kfhbegrrB8CKhJDUxmHbZsyQUmjf/iX57/LhuWj1xGhRL8AA==
+ dependencies:
+ "@types/koa" "^2.11.6"
+ "@types/ws" "^7.4.0"
+ "@web/parse5-utils" "^1.2.0"
+ chokidar "^3.4.3"
+ clone "^2.1.2"
+ es-module-lexer "^0.9.0"
+ get-stream "^6.0.0"
+ is-stream "^2.0.0"
+ isbinaryfile "^4.0.6"
+ koa "^2.13.0"
+ koa-etag "^4.0.0"
+ koa-send "^5.0.1"
+ koa-static "^5.0.0"
+ lru-cache "^6.0.0"
+ mime-types "^2.1.27"
+ parse5 "^6.0.1"
+ picomatch "^2.2.2"
+ ws "^7.4.2"
+
+"@web/parse5-utils@^1.2.0":
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/@web/parse5-utils/-/parse5-utils-1.3.0.tgz#e2e9e98b31a4ca948309f74891bda8d77399f6bd"
+ integrity sha512-Pgkx3ECc8EgXSlS5EyrgzSOoUbM6P8OKS471HLAyvOBcP1NCBn0to4RN/OaKASGq8qa3j+lPX9H14uA5AHEnQg==
+ dependencies:
+ "@types/parse5" "^6.0.1"
+ parse5 "^6.0.1"
+
+"@web/test-runner-commands@^0.5.7":
+ version "0.5.13"
+ resolved "https://registry.yarnpkg.com/@web/test-runner-commands/-/test-runner-commands-0.5.13.tgz#57ea472c00ee2ada99eb9bb5a0371200922707c2"
+ integrity sha512-FXnpUU89ALbRlh9mgBd7CbSn5uzNtr8gvnQZPOvGLDAJ7twGvZdUJEAisPygYx2BLPSFl3/Mre8pH8zshJb8UQ==
+ dependencies:
+ "@web/test-runner-core" "^0.10.20"
+ mkdirp "^1.0.4"
+
+"@web/test-runner-core@^0.10.20":
+ version "0.10.22"
+ resolved "https://registry.yarnpkg.com/@web/test-runner-core/-/test-runner-core-0.10.22.tgz#34bb67d12a79b01dc79c816f3d76f3419ef50eaf"
+ integrity sha512-0jzJIl/PTZa6PCG/noHAFZT2DTcp+OYGmYOnZ2wcHAO3KwtJKnBVSuxgdOzFdmfvoO7TYAXo5AH+MvTZXMWsZw==
+ dependencies:
+ "@babel/code-frame" "^7.12.11"
+ "@types/babel__code-frame" "^7.0.2"
+ "@types/co-body" "^6.1.0"
+ "@types/convert-source-map" "^1.5.1"
+ "@types/debounce" "^1.2.0"
+ "@types/istanbul-lib-coverage" "^2.0.3"
+ "@types/istanbul-reports" "^3.0.0"
+ "@web/browser-logs" "^0.2.1"
+ "@web/dev-server-core" "^0.3.16"
+ chokidar "^3.4.3"
+ cli-cursor "^3.1.0"
+ co-body "^6.1.0"
+ convert-source-map "^1.7.0"
+ debounce "^1.2.0"
+ dependency-graph "^0.11.0"
+ globby "^11.0.1"
+ ip "^1.1.5"
+ istanbul-lib-coverage "^3.0.0"
+ istanbul-lib-report "^3.0.0"
+ istanbul-reports "^3.0.2"
+ log-update "^4.0.0"
+ nanocolors "^0.2.1"
+ nanoid "^3.1.25"
+ open "^8.0.2"
+ picomatch "^2.2.2"
+ source-map "^0.7.3"
+
+"@webcomponents/scoped-custom-element-registry@^0.0.3":
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/@webcomponents/scoped-custom-element-registry/-/scoped-custom-element-registry-0.0.3.tgz#774591a886b0b0e4914717273ba53fd8d5657522"
+ integrity sha512-lpSzgDCGbM99dytb3+J3Suo4+Bk1E13MPnWB42JK8GwxSAxFz+tC7TTv2hhDSIE2IirGNKNKCf3m08ecu6eAsQ==
+
"@webcomponents/shadycss@^1.10.2", "@webcomponents/shadycss@^1.9.1":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@webcomponents/shadycss/-/shadycss-1.11.0.tgz#73e289996c002d8be694cd3be0e83c46ad25e7e0"
@@ -1351,6 +1571,13 @@
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
+ansi-escapes@^4.3.0:
+ version "4.3.2"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
+ integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
+ dependencies:
+ type-fest "^0.21.3"
+
ansi-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
@@ -1408,6 +1635,11 @@
resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e"
integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==
+array-union@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
+ integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
+
arrify@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
@@ -1430,6 +1662,11 @@
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==
+astral-regex@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
+ integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
+
async@^2.6.2:
version "2.6.3"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
@@ -1498,10 +1735,10 @@
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
-base64-arraybuffer@0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812"
- integrity sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=
+base64-arraybuffer@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz#87bd13525626db4a9838e00a508c2b73efcf348c"
+ integrity sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA==
base64id@2.0.0, base64id@~2.0.0:
version "2.0.0"
@@ -1544,7 +1781,7 @@
balanced-match "^1.0.0"
concat-map "0.0.1"
-braces@^3.0.2, braces@~3.0.2:
+braces@^3.0.1, braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
@@ -1716,6 +1953,13 @@
dependencies:
source-map "~0.6.0"
+cli-cursor@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
+ integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
+ dependencies:
+ restore-cursor "^3.1.0"
+
cliui@^7.0.2:
version "7.0.4"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
@@ -1730,6 +1974,16 @@
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
+co-body@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/co-body/-/co-body-6.1.0.tgz#d87a8efc3564f9bfe3aced8ef5cd04c7a8766547"
+ integrity sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==
+ dependencies:
+ inflation "^2.0.0"
+ qs "^6.5.2"
+ raw-body "^2.3.3"
+ type-is "^1.6.16"
+
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -1939,7 +2193,7 @@
dependencies:
ms "^2.1.1"
-debug@^4.1.0, debug@^4.1.1, debug@~4.3.1:
+debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@~4.3.1, debug@~4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
@@ -1980,6 +2234,11 @@
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
+define-lazy-prop@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
+ integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==
+
define-properties@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
@@ -2007,6 +2266,11 @@
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
+dependency-graph@^0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27"
+ integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==
+
destroy@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
@@ -2027,6 +2291,13 @@
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
+dir-glob@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
+ integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
+ dependencies:
+ path-type "^4.0.0"
+
dom-serialize@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
@@ -2085,25 +2356,28 @@
dependencies:
once "^1.4.0"
-engine.io-parser@~4.0.0:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-4.0.2.tgz#e41d0b3fb66f7bf4a3671d2038a154024edb501e"
- integrity sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==
+engine.io-parser@~5.0.0:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.1.tgz#6695fc0f1e6d76ad4a48300ff80db5f6b3654939"
+ integrity sha512-j4p3WwJrG2k92VISM0op7wiq60vO92MlF3CRGxhKHy9ywG1/Dkc72g0dXeDQ+//hrcDn8gqQzoEkdO9FN0d9AA==
dependencies:
- base64-arraybuffer "0.1.4"
+ base64-arraybuffer "~1.0.1"
-engine.io@~4.1.0:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-4.1.1.tgz#9a8f8a5ac5a5ea316183c489bf7f5b6cf91ace5b"
- integrity sha512-t2E9wLlssQjGw0nluF6aYyfX8LwYU8Jj0xct+pAhfWfv/YrBn6TSNtEYsgxHIfaMqfrLx07czcMg9bMN6di+3w==
+engine.io@~6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.0.0.tgz#2b993fcd73e6b3a6abb52b40b803651cd5747cf0"
+ integrity sha512-Ui7yl3JajEIaACg8MOUwWvuuwU7jepZqX3BKs1ho7NQRuP4LhN4XIykXhp8bEy+x/DhA0LBZZXYSCkZDqrwMMg==
dependencies:
+ "@types/cookie" "^0.4.1"
+ "@types/cors" "^2.8.12"
+ "@types/node" ">=10.0.0"
accepts "~1.3.4"
base64id "2.0.0"
cookie "~0.4.1"
cors "~2.8.5"
debug "~4.3.1"
- engine.io-parser "~4.0.0"
- ws "~7.4.2"
+ engine.io-parser "~5.0.0"
+ ws "~8.2.3"
ent@~2.2.0:
version "2.2.0"
@@ -2117,6 +2391,11 @@
dependencies:
is-arrayish "^0.2.1"
+errorstacks@^2.2.0:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/errorstacks/-/errorstacks-2.3.2.tgz#cab2c7c83e199a2b2862de3fea46f68372094166"
+ integrity sha512-cJp8qf5t2cXmVZJjZVrcU4ODFJeQOcUyjJEtPFtWO+3N6JPM6vCe4Sfv3cwIs/qS7gnUo/fvKX/mDCVQZq+P7A==
+
es-dev-server@^1.57.0:
version "1.60.2"
resolved "https://registry.yarnpkg.com/es-dev-server/-/es-dev-server-1.60.2.tgz#cca56fe452d46c3ec531c19745e0aa9d7b71e1b3"
@@ -2193,6 +2472,11 @@
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.3.26.tgz#7b507044e97d5b03b01d4392c74ffeb9c177a83b"
integrity sha512-Va0Q/xqtrss45hWzP8CZJwzGSZJjDM5/MJRE3IXXnUCcVLElR9BRaE9F62BopysASyc4nM3uwhSW7FFB9nlWAA==
+es-module-lexer@^0.9.0:
+ version "0.9.3"
+ resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19"
+ integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==
+
es-module-shims@^0.4.6, es-module-shims@^0.4.7:
version "0.4.7"
resolved "https://registry.yarnpkg.com/es-module-shims/-/es-module-shims-0.4.7.tgz#1419b65bbd38dfe91ab8ea5d7b4b454561e44641"
@@ -2228,7 +2512,7 @@
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
-etag@^1.3.0:
+etag@^1.3.0, etag@^1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
@@ -2258,11 +2542,29 @@
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+fast-glob@^3.1.1:
+ version "3.2.7"
+ resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1"
+ integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==
+ dependencies:
+ "@nodelib/fs.stat" "^2.0.2"
+ "@nodelib/fs.walk" "^1.2.3"
+ glob-parent "^5.1.2"
+ merge2 "^1.3.0"
+ micromatch "^4.0.4"
+
fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+fastq@^1.6.0:
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
+ integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==
+ dependencies:
+ reusify "^1.0.4"
+
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
@@ -2394,6 +2696,11 @@
dependencies:
pump "^3.0.0"
+get-stream@^6.0.0:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
+ integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
+
getpass@^0.1.1:
version "0.1.7"
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
@@ -2401,7 +2708,7 @@
dependencies:
assert-plus "^1.0.0"
-glob-parent@~5.1.0, glob-parent@~5.1.2:
+glob-parent@^5.1.2, glob-parent@~5.1.0, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
@@ -2437,6 +2744,18 @@
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
+globby@^11.0.1:
+ version "11.0.4"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5"
+ integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==
+ dependencies:
+ array-union "^2.1.0"
+ dir-glob "^3.0.1"
+ fast-glob "^3.1.1"
+ ignore "^5.1.4"
+ merge2 "^1.3.0"
+ slash "^3.0.0"
+
graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.6:
version "4.2.8"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
@@ -2499,6 +2818,11 @@
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
+html-escaper@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
+ integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
+
html-minifier-terser@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#922e96f1f3bb60832c2634b79884096389b1f054"
@@ -2531,6 +2855,17 @@
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
+http-errors@1.7.3, http-errors@~1.7.2:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
+ integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
+ dependencies:
+ depd "~1.1.2"
+ inherits "2.0.4"
+ setprototypeof "1.1.1"
+ statuses ">= 1.5.0 < 2"
+ toidentifier "1.0.0"
+
http-errors@^1.6.3, http-errors@^1.7.3:
version "1.8.0"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507"
@@ -2552,17 +2887,6 @@
setprototypeof "1.1.0"
statuses ">= 1.4.0 < 2"
-http-errors@~1.7.2:
- version "1.7.3"
- resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
- integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
- dependencies:
- depd "~1.1.2"
- inherits "2.0.4"
- setprototypeof "1.1.1"
- statuses ">= 1.5.0 < 2"
- toidentifier "1.0.0"
-
http-proxy@^1.18.1:
version "1.18.1"
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
@@ -2588,6 +2912,16 @@
dependencies:
safer-buffer ">= 2.1.2 < 3"
+ignore@^5.1.4:
+ version "5.1.9"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.9.tgz#9ec1a5cbe8e1446ec60d4420060d43aa6e7382fb"
+ integrity sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==
+
+inflation@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/inflation/-/inflation-2.0.0.tgz#8b417e47c28f925a45133d914ca1fd389107f30f"
+ integrity sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=
+
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@@ -2611,6 +2945,11 @@
resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.7.0.tgz#ee16bee978db53516ead2f0a8154b09b400bbdc9"
integrity sha512-Id0Fij0HsB/vKWGeBe9PxeY45ttRiBmhFyyt/geBdDHBYNctMRTE3dC1U3ujzz3lap+hVXlEcVaB56kZP/eEUg==
+ip@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
+ integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
+
is-arrayish@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
@@ -2630,7 +2969,7 @@
dependencies:
has "^1.0.3"
-is-docker@^2.0.0:
+is-docker@^2.0.0, is-docker@^2.1.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
@@ -2689,7 +3028,7 @@
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
-is-wsl@^2.1.1:
+is-wsl@^2.1.1, is-wsl@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
@@ -2701,7 +3040,7 @@
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
-isbinaryfile@^4.0.2, isbinaryfile@^4.0.8:
+isbinaryfile@^4.0.2, isbinaryfile@^4.0.6, isbinaryfile@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.8.tgz#5d34b94865bd4946633ecc78a026fc76c5b11fcf"
integrity sha512-53h6XFniq77YdW+spoRrebh0mnmTxRPTlcuIArO57lmMdq4uBKFKaeTjnb92oYWrSn/LVL+LT+Hap2tFQj8V+w==
@@ -2721,6 +3060,11 @@
resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49"
integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==
+istanbul-lib-coverage@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3"
+ integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==
+
istanbul-lib-instrument@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630"
@@ -2734,6 +3078,23 @@
istanbul-lib-coverage "^2.0.5"
semver "^6.0.0"
+istanbul-lib-report@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6"
+ integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==
+ dependencies:
+ istanbul-lib-coverage "^3.0.0"
+ make-dir "^3.0.0"
+ supports-color "^7.1.0"
+
+istanbul-reports@^3.0.2:
+ version "3.0.5"
+ resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.5.tgz#a2580107e71279ea6d661ddede929ffc6d693384"
+ integrity sha512-5+19PlhnGabNWB7kOFnuxT8H3T/iIyQzIbQMxXsURmmvKg86P2sbkrGOT77VnHw0Qr0gc2XzRaRfMZYYbSQCJQ==
+ dependencies:
+ html-escaper "^2.0.0"
+ istanbul-lib-report "^3.0.0"
+
js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -2833,10 +3194,10 @@
dependencies:
minimist "^1.2.3"
-karma@^6.3.4:
- version "6.3.4"
- resolved "https://registry.yarnpkg.com/karma/-/karma-6.3.4.tgz#359899d3aab3d6b918ea0f57046fd2a6b68565e6"
- integrity sha512-hbhRogUYIulfkBTZT7xoPrCYhRBnBoqbbL4fszWD0ReFGUxU+LYBr3dwKdAluaDQ/ynT9/7C+Lf7pPNW4gSx4Q==
+karma@^6.3.6:
+ version "6.3.6"
+ resolved "https://registry.yarnpkg.com/karma/-/karma-6.3.6.tgz#6f64cdd558c7d0c9da6fcdece156089582694611"
+ integrity sha512-xsiu3D6AjCv6Uq0YKXJgC6TvXX2WloQ5+XtHXmC1lwiLVG617DDV3W2DdM4BxCMKHlmz6l3qESZHFQGHAKvrew==
dependencies:
body-parser "^1.19.0"
braces "^3.0.2"
@@ -2856,10 +3217,10 @@
qjobs "^1.2.0"
range-parser "^1.2.1"
rimraf "^3.0.2"
- socket.io "^3.1.0"
+ socket.io "^4.2.0"
source-map "^0.6.1"
tmp "^0.2.1"
- ua-parser-js "^0.7.28"
+ ua-parser-js "^0.7.30"
yargs "^16.1.1"
keygrip@~1.1.0:
@@ -2899,6 +3260,14 @@
co "^4.6.0"
koa-compose "^3.0.0"
+koa-convert@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-2.0.0.tgz#86a0c44d81d40551bae22fee6709904573eea4f5"
+ integrity sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==
+ dependencies:
+ co "^4.6.0"
+ koa-compose "^4.1.0"
+
koa-etag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/koa-etag/-/koa-etag-3.0.0.tgz#9ef7382ddd5a82ab0deb153415c915836f771d3f"
@@ -2907,12 +3276,19 @@
etag "^1.3.0"
mz "^2.1.0"
+koa-etag@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/koa-etag/-/koa-etag-4.0.0.tgz#2c2bb7ae69ca1ac6ced09ba28dcb78523c810414"
+ integrity sha512-1cSdezCkBWlyuB9l6c/IFoe1ANCDdPBxkDkRiaIup40xpUub6U/wwRXoKBZw/O5BifX9OlqAjYnDyzM6+l+TAg==
+ dependencies:
+ etag "^1.8.1"
+
koa-is-json@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14"
integrity sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=
-koa-send@^5.0.0:
+koa-send@^5.0.0, koa-send@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-5.0.1.tgz#39dceebfafb395d0d60beaffba3a70b4f543fe79"
integrity sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==
@@ -2929,6 +3305,35 @@
debug "^3.1.0"
koa-send "^5.0.0"
+koa@^2.13.0:
+ version "2.13.4"
+ resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.4.tgz#ee5b0cb39e0b8069c38d115139c774833d32462e"
+ integrity sha512-43zkIKubNbnrULWlHdN5h1g3SEKXOEzoAlRsHOTFpnlDu8JlAOZSMJBLULusuXRequboiwJcj5vtYXKB3k7+2g==
+ dependencies:
+ accepts "^1.3.5"
+ cache-content-type "^1.0.0"
+ content-disposition "~0.5.2"
+ content-type "^1.0.4"
+ cookies "~0.8.0"
+ debug "^4.3.2"
+ delegates "^1.0.0"
+ depd "^2.0.0"
+ destroy "^1.0.4"
+ encodeurl "^1.0.2"
+ escape-html "^1.0.3"
+ fresh "~0.5.2"
+ http-assert "^1.3.0"
+ http-errors "^1.6.3"
+ is-generator-function "^1.0.7"
+ koa-compose "^4.1.0"
+ koa-convert "^2.0.0"
+ on-finished "^2.3.0"
+ only "~0.0.2"
+ parseurl "^1.3.2"
+ statuses "^1.5.0"
+ type-is "^1.6.16"
+ vary "^1.1.2"
+
koa@^2.7.0:
version "2.13.1"
resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.1.tgz#6275172875b27bcfe1d454356a5b6b9f5a9b1051"
@@ -2958,6 +3363,30 @@
type-is "^1.6.16"
vary "^1.1.2"
+lit-element@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.0.2.tgz#6422b68ba166a32695f524d6f3eb41712610bf50"
+ integrity sha512-9vTJ47D2DSE4Jwhle7aMzEwO2ZcOPRikqfT3CVG7Qol2c9/I4KZwinZNW5Xv8hNm+G/enSSfIwqQhIXi6ioAUg==
+ dependencies:
+ "@lit/reactive-element" "^1.0.0"
+ lit-html "^2.0.0"
+
+lit-html@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.0.2.tgz#6a17caac4135757710c5fb3e4becc622c476e431"
+ integrity sha512-dON7Zg8btb14/fWohQLQBdSgkoiQA4mIUy87evmyJHtxRq7zS6LlC32bT5EPWiof5PUQaDpF45v2OlrxHA5Clg==
+ dependencies:
+ "@types/trusted-types" "^2.0.2"
+
+lit@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/lit/-/lit-2.0.2.tgz#5e6f422924e0732258629fb379556b6d23f7179c"
+ integrity sha512-hKA/1YaSB+P+DvKWuR2q1Xzy/iayhNrJ3aveD0OQ9CKn6wUjsdnF/7LavDOJsKP/K5jzW/kXsuduPgRvTFrFJw==
+ dependencies:
+ "@lit/reactive-element" "^1.0.0"
+ lit-element "^3.0.0"
+ lit-html "^2.0.0"
+
load-json-file@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
@@ -3032,6 +3461,16 @@
dependencies:
chalk "^2.0.1"
+log-update@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1"
+ integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==
+ dependencies:
+ ansi-escapes "^4.3.0"
+ cli-cursor "^3.1.0"
+ slice-ansi "^4.0.0"
+ wrap-ansi "^6.2.0"
+
log4js@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.3.0.tgz#10dfafbb434351a3e30277a00b9879446f715bcb"
@@ -3072,11 +3511,31 @@
dependencies:
yallist "^4.0.0"
+make-dir@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
+ integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
+ dependencies:
+ semver "^6.0.0"
+
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
+merge2@^1.3.0:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+ integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+micromatch@^4.0.4:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
+ integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
+ dependencies:
+ braces "^3.0.1"
+ picomatch "^2.2.3"
+
mime-db@1.49.0, "mime-db@>= 1.43.0 < 2":
version "1.49.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed"
@@ -3094,6 +3553,11 @@
resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe"
integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==
+mimic-fn@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
+ integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+
minimatch@3.0.4, minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
@@ -3113,6 +3577,11 @@
dependencies:
minimist "^1.2.5"
+mkdirp@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
+ integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
+
mocha@8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.3.2.tgz#53406f195fa86fbdebe71f8b1c6fb23221d69fcc"
@@ -3168,11 +3637,21 @@
object-assign "^4.0.1"
thenify-all "^1.0.0"
+nanocolors@^0.2.1:
+ version "0.2.13"
+ resolved "https://registry.yarnpkg.com/nanocolors/-/nanocolors-0.2.13.tgz#dfd1ed0bfab05e9fe540eb6874525f0a1684099b"
+ integrity sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==
+
nanoid@3.1.20:
version "3.1.20"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
+nanoid@^3.1.25:
+ version "3.1.30"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362"
+ integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==
+
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
@@ -3232,6 +3711,11 @@
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+object-inspect@^1.9.0:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1"
+ integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==
+
object-keys@^1.0.12, object-keys@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
@@ -3261,6 +3745,13 @@
dependencies:
wrappy "1"
+onetime@^5.1.0:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
+ integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
+ dependencies:
+ mimic-fn "^2.1.0"
+
only@~0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
@@ -3274,6 +3765,15 @@
is-docker "^2.0.0"
is-wsl "^2.1.1"
+open@^8.0.2:
+ version "8.4.0"
+ resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8"
+ integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==
+ dependencies:
+ define-lazy-prop "^2.0.0"
+ is-docker "^2.1.1"
+ is-wsl "^2.2.0"
+
os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
@@ -3333,6 +3833,11 @@
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
+parse5@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
+ integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
+
parseurl@^1.3.2, parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
@@ -3385,6 +3890,11 @@
dependencies:
pify "^3.0.0"
+path-type@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
+ integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+
pathval@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d"
@@ -3395,7 +3905,7 @@
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
-picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2:
+picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3:
version "2.3.0"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
@@ -3468,11 +3978,23 @@
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
+qs@^6.5.2:
+ version "6.10.1"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a"
+ integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==
+ dependencies:
+ side-channel "^1.0.4"
+
qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
+queue-microtask@^1.2.2:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
+ integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@@ -3495,6 +4017,16 @@
iconv-lite "0.4.24"
unpipe "1.0.0"
+raw-body@^2.3.3:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c"
+ integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==
+ dependencies:
+ bytes "3.1.0"
+ http-errors "1.7.3"
+ iconv-lite "0.4.24"
+ unpipe "1.0.0"
+
read-pkg-up@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978"
@@ -3646,6 +4178,19 @@
is-core-module "^2.2.0"
path-parse "^1.0.6"
+restore-cursor@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
+ integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
+ dependencies:
+ onetime "^5.1.0"
+ signal-exit "^3.0.2"
+
+reusify@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
+ integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
rfdc@^1.1.4:
version "1.3.0"
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
@@ -3665,6 +4210,13 @@
optionalDependencies:
fsevents "~2.3.2"
+run-parallel@^1.1.9:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
+ integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
+ dependencies:
+ queue-microtask "^1.2.2"
+
safe-buffer@5.1.2, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@@ -3729,6 +4281,20 @@
resolved "https://registry.yarnpkg.com/shady-css-scoped-element/-/shady-css-scoped-element-0.0.2.tgz#c538fcfe2317e979cd02dfec533898b95b4ea8fe"
integrity sha512-Dqfl70x6JiwYDujd33ZTbtCK0t52E7+H2swdWQNSTzfsolSa6LJHnTpN4T9OpJJEq4bxuzHRLFO9RBcy/UfrMQ==
+side-channel@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
+ integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
+ dependencies:
+ call-bind "^1.0.0"
+ get-intrinsic "^1.0.2"
+ object-inspect "^1.9.0"
+
+signal-exit@^3.0.2:
+ version "3.0.5"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f"
+ integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==
+
sinon@^10.0.0:
version "10.0.1"
resolved "https://registry.yarnpkg.com/sinon/-/sinon-10.0.1.tgz#0d1a13ecb86f658d15984f84273e57745b1f4c57"
@@ -3741,12 +4307,26 @@
nise "^5.0.1"
supports-color "^7.1.0"
-socket.io-adapter@~2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz#edc5dc36602f2985918d631c1399215e97a1b527"
- integrity sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg==
+slash@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
+ integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
-socket.io-parser@~4.0.3:
+slice-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
+ integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==
+ dependencies:
+ ansi-styles "^4.0.0"
+ astral-regex "^2.0.0"
+ is-fullwidth-code-point "^3.0.0"
+
+socket.io-adapter@~2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.3.2.tgz#039cd7c71a52abad984a6d57da2c0b7ecdd3c289"
+ integrity sha512-PBZpxUPYjmoogY0aoaTmo1643JelsaS1CiAwNjRVdrI0X9Seuc19Y2Wife8k88avW6haG8cznvwbubAZwH4Mtg==
+
+socket.io-parser@~4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0"
integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==
@@ -3755,20 +4335,17 @@
component-emitter "~1.3.0"
debug "~4.3.1"
-socket.io@^3.1.0:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-3.1.2.tgz#06e27caa1c4fc9617547acfbb5da9bc1747da39a"
- integrity sha512-JubKZnTQ4Z8G4IZWtaAZSiRP3I/inpy8c/Bsx2jrwGrTbKeVU5xd6qkKMHpChYeM3dWZSO0QACiGK+obhBNwYw==
+socket.io@^4.2.0:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.3.1.tgz#c0aa14f3f916a8ab713e83a5bd20c16600245763"
+ integrity sha512-HC5w5Olv2XZ0XJ4gOLGzzHEuOCfj3G0SmoW3jLHYYh34EVsIr3EkW9h6kgfW+K3TFEcmYy8JcPWe//KUkBp5jA==
dependencies:
- "@types/cookie" "^0.4.0"
- "@types/cors" "^2.8.8"
- "@types/node" ">=10.0.0"
accepts "~1.3.4"
base64id "~2.0.0"
- debug "~4.3.1"
- engine.io "~4.1.0"
- socket.io-adapter "~2.1.0"
- socket.io-parser "~4.0.3"
+ debug "~4.3.2"
+ engine.io "~6.0.0"
+ socket.io-adapter "~2.3.2"
+ socket.io-parser "~4.0.4"
source-map-support@^0.5.19, source-map-support@~0.5.12:
version "0.5.19"
@@ -3788,6 +4365,11 @@
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+source-map@^0.7.3:
+ version "0.7.3"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
+ integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
+
spdx-correct@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
@@ -4038,6 +4620,11 @@
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
+type-fest@^0.21.3:
+ version "0.21.3"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
+ integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
+
type-is@^1.6.16, type-is@~1.6.17:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
@@ -4056,10 +4643,10 @@
resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066"
integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==
-ua-parser-js@^0.7.28:
- version "0.7.28"
- resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
- integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
+ua-parser-js@^0.7.30:
+ version "0.7.30"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.30.tgz#4cf5170e8b55ac553fe8b38df3a82f0669671f0b"
+ integrity sha512-uXEtSresNUlXQ1QL4/3dQORcGv7+J2ookOG2ybA/ga9+HYEXueT2o+8dUJQkpedsyTyCJ6jCCirRcKtdtx1kbg==
unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4"
@@ -4204,6 +4791,15 @@
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.0.tgz#a8e038b4c94569596852de7a8ea4228eefdeb37b"
integrity sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==
+wrap-ansi@^6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
+ integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
@@ -4218,10 +4814,15 @@
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
-ws@~7.4.2:
- version "7.4.6"
- resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
- integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
+ws@^7.4.2:
+ version "7.5.5"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881"
+ integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==
+
+ws@~8.2.3:
+ version "8.2.3"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
+ integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
y18n@^5.0.5:
version "5.0.8"
diff --git a/proto/cache.proto b/proto/cache.proto
index 16e5e95..950e63e 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -448,7 +448,7 @@
}
// Serialized form of com.google.gerrit.common.data.LabelType.
-// Next ID: 21
+// Next ID: 22
message LabelTypeProto {
string name = 1;
string function = 2; // ENUM as String
@@ -470,6 +470,7 @@
repeated string ref_patterns = 18;
bool copy_all_scores_if_list_of_files_did_not_change = 19;
string copy_condition = 20;
+ string description = 21;
}
// Serialized form of com.google.gerrit.entities.SubmitRequirement.
@@ -484,7 +485,7 @@
}
// Serialized form of com.google.gerrit.entities.SubmitRequirementResult.
-// Next ID: 7
+// Next ID: 8
message SubmitRequirementResultProto {
SubmitRequirementProto submit_requirement = 1;
SubmitRequirementExpressionResultProto applicability_expression_result = 2;
@@ -496,6 +497,10 @@
// Whether this result was created from a legacy submit record.
bool legacy = 6;
+
+ // Whether the submit requirement was bypassed during submission (i.e. by
+ // performing a push with the %submit option).
+ bool forced = 7;
}
// Serialized form of com.google.gerrit.entities.SubmitRequirementExpressionResult.
@@ -718,18 +723,3 @@
ComparisonType comparison_type = 11;
bool negative = 12;
}
-
-// Serialized form of com.google.gerrit.server.approval.ApprovalCacheImpl.Key.
-// Next ID: 5
-message PatchSetApprovalsKeyProto {
- string project = 1;
- int32 change_id = 2;
- int32 patch_set_id = 3;
- bytes id = 4;
-}
-
-// Repeated version of PatchSetApprovalProto
-// Next ID: 2
-message AllPatchSetApprovalsProto {
- repeated devtools.gerritcodereview.PatchSetApproval approval = 1;
-}
diff --git a/proto/entities.proto b/proto/entities.proto
index de8f647..191cca7 100644
--- a/proto/entities.proto
+++ b/proto/entities.proto
@@ -125,7 +125,7 @@
}
// Serialized form of com.google.gerrit.entities.PatchSetApproval.
-// Next ID: 9
+// Next ID: 11
message PatchSetApproval {
required PatchSetApproval_Key key = 1;
optional int32 value = 2;
@@ -134,6 +134,7 @@
optional Account_Id real_account_id = 7;
optional bool post_submit = 8;
optional bool copied = 9;
+ optional string uuid = 10;
// Deleted fields, should not be reused:
reserved 4; // changeOpen
diff --git a/resources/BUILD b/resources/BUILD
index b53ae4c..d4d0df3 100644
--- a/resources/BUILD
+++ b/resources/BUILD
@@ -11,5 +11,5 @@
name = "log4j-config__jar",
srcs = ["log4j.properties"],
outs = ["log4j-config.jar"],
- cmd = "cd resources && zip -9Dqr $$ROOT/$@ .",
+ cmd = "cd $$(dirname $(location log4j.properties)) && zip -9Dqr $$ROOT/$@ .",
)
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index 11717fb..c7b3d9e 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -29,7 +29,6 @@
{@param? useGoogleFonts: ?}
{@param? changeRequestsPath: ?}
{@param? defaultChangeDetailHex: ?}
- {@param? defaultDiffDetailHex: ?}
{@param? defaultDashboardHex: ?}
{@param? dashboardQuery: ?}
{@param? userIsAuthenticated: ?}
@@ -52,9 +51,6 @@
{if $defaultChangeDetailHex}
changePage: '{$defaultChangeDetailHex}',
{/if}
- {if $defaultDiffDetailHex}
- diffPage: '{$defaultDiffDetailHex}',
- {/if}
{if $defaultDashboardHex}
dashboardPage: '{$defaultDashboardHex}',
{/if}
@@ -99,18 +95,11 @@
<link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/edit/?download-commands=true" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
{/if}
{/if}
- {if $defaultDiffDetailHex}
- <link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/detail?O={$defaultDiffDetailHex}" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
- {if $userIsAuthenticated}
- <link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/edit/" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
- {/if}
- <link rel="preload" href="{$staticResourcePath}/bower_components/highlightjs/highlight.min.js" as="script"/>
- {/if}
- <link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/comments" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
+ <link rel="preload" href="{$staticResourcePath}/bower_components/highlightjs/highlight.min.js" as="script" crossorigin="anonymous"/>
<link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/comments?enable-context=true&context-padding=3" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
<link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/robotcomments" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
{if $userIsAuthenticated}
- <link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/drafts" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
+ <link rel="preload" href="{$canonicalPath}/{$changeRequestsPath}/drafts?enable-context=true&context-padding=3" as="fetch" type="application/json" crossorigin="anonymous"/>{\n}
{/if}
{/if}
{if $userIsAuthenticated and $defaultDashboardHex and $dashboardQuery}
diff --git a/resources/com/google/gerrit/server/mime/mime-types.properties b/resources/com/google/gerrit/server/mime/mime-types.properties
index fd2280c..01ca71c 100644
--- a/resources/com/google/gerrit/server/mime/mime-types.properties
+++ b/resources/com/google/gerrit/server/mime/mime-types.properties
@@ -67,6 +67,7 @@
f = text/x-fortran
factor = text/x-factor
feathre = text/x-feature
+feature = text/x-gherkin
fcl = text/x-fcl
for = text/x-fortran
formula = text/x-spreadsheet
diff --git a/tools/BUILD b/tools/BUILD
index 47a2a2e..04375f9 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -1,22 +1,11 @@
load(
"@bazel_tools//tools/jdk:default_java_toolchain.bzl",
- "JDK9_JVM_OPTS",
"default_java_toolchain",
)
load("@rules_java//java:defs.bzl", "java_package_configuration")
exports_files(["nongoogle.bzl"])
-default_java_toolchain(
- name = "error_prone_warnings_toolchain",
- bootclasspath = ["@bazel_tools//tools/jdk:platformclasspath.jar"],
- jvm_opts = JDK9_JVM_OPTS,
- package_configuration = [
- ":error_prone",
- ],
- visibility = ["//visibility:public"],
-)
-
JDK11_JVM_OPTS = select({
"@bazel_tools//src/conditions:openbsd": ["-Xbootclasspath/p:$(location @bazel_tools//tools/jdk:javac_jar)"],
"//conditions:default": [
@@ -102,7 +91,7 @@
"-Xep:AsyncFunctionReturnsNull:ERROR",
"-Xep:AutoValueConstructorOrderChecker:ERROR",
"-Xep:AutoValueFinalMethods:ERROR",
- # "-Xep:AutoValueImmutableFields:WARN",
+ "-Xep:AutoValueImmutableFields:ERROR",
# "-Xep:AutoValueSubclassLeaked:WARN",
"-Xep:BadAnnotationImplementation:ERROR",
"-Xep:BadComparable:ERROR",
@@ -119,7 +108,7 @@
"-Xep:CacheLoaderNull:ERROR",
"-Xep:CannotMockFinalClass:ERROR",
"-Xep:CanonicalDuration:ERROR",
- # "-Xep:CatchAndPrintStackTrace:WARN",
+ "-Xep:CatchAndPrintStackTrace:ERROR",
"-Xep:CatchFail:ERROR",
"-Xep:ChainedAssertionLosesContext:ERROR",
"-Xep:ChainingConstructorIgnoresParameter:ERROR",
@@ -151,7 +140,7 @@
"-Xep:DeadException:ERROR",
"-Xep:DeadThread:ERROR",
"-Xep:DefaultCharset:ERROR",
- # "-Xep:DefaultPackage:WARN",
+ "-Xep:DefaultPackage:ERROR",
"-Xep:DepAnn:ERROR",
"-Xep:DeprecatedVariable:ERROR",
"-Xep:DiscardedPostfixExpression:ERROR",
@@ -168,9 +157,9 @@
"-Xep:DurationTemporalUnit:ERROR",
"-Xep:DurationToLongTimeUnit:ERROR",
"-Xep:EmptyBlockTag:ERROR",
- # "-Xep:EmptyCatch:WARN",
+ "-Xep:EmptyCatch:ERROR",
"-Xep:EmptySetMultibindingContributions:ERROR",
- # "-Xep:EqualsGetClass:WARN",
+ "-Xep:EqualsGetClass:ERROR",
"-Xep:EqualsHashCode:ERROR",
"-Xep:EqualsIncompatibleType:ERROR",
"-Xep:EqualsNaN:ERROR",
@@ -180,7 +169,7 @@
"-Xep:EqualsUsingHashCode:ERROR",
"-Xep:EqualsWrongThing:ERROR",
"-Xep:ErroneousThreadPoolConstructorChecker:ERROR",
- # "-Xep:EscapedEntity:WARN",
+ "-Xep:EscapedEntity:WARN",
"-Xep:ExpectedExceptionChecker:ERROR",
"-Xep:ExtendingJUnitAssert:ERROR",
"-Xep:ExtendsAutoValue:ERROR",
@@ -223,7 +212,7 @@
"-Xep:Incomparable:ERROR",
"-Xep:IncompatibleArgumentType:ERROR",
"-Xep:IncompatibleModifiers:ERROR",
- # "-Xep:InconsistentCapitalization:WARN",
+ "-Xep:InconsistentCapitalization:ERROR",
"-Xep:InconsistentHashCode:ERROR",
"-Xep:IncrementInForLoopAndHeader:ERROR",
"-Xep:IndexOfChar:ERROR",
@@ -231,7 +220,7 @@
"-Xep:InfiniteRecursion:ERROR",
"-Xep:InjectOnConstructorOfAbstractClass:ERROR",
"-Xep:InheritDoc:ERROR",
- # "-Xep:InlineFormatString:WARN",
+ "-Xep:InlineFormatString:ERROR",
"-Xep:InlineMeInliner:ERROR",
"-Xep:InlineMeSuggester:ERROR",
"-Xep:InlineMeValidator:ERROR",
@@ -244,7 +233,7 @@
"-Xep:InvalidInlineTag:ERROR",
"-Xep:InvalidJavaTimeConstant:ERROR",
"-Xep:InvalidLink:ERROR",
- # "-Xep:InvalidParam:WARN",
+ "-Xep:InvalidParam:ERROR",
"-Xep:InvalidPatternSyntax:ERROR",
"-Xep:InvalidThrows:ERROR",
"-Xep:InvalidThrowsLink:ERROR",
@@ -275,8 +264,8 @@
"-Xep:JavaLocalTimeGetNano:ERROR",
"-Xep:JavaPeriodGetDays:ERROR",
"-Xep:JavaTimeDefaultTimeZone:ERROR",
- "-Xep:JavaUtilDate:ERROR",
- # "-Xep:JdkObsolete:WARN",
+ "-Xep:JavaUtilDate:WARN",
+ "-Xep:JdkObsolete:ERROR",
"-Xep:JodaConstructors:ERROR",
"-Xep:JodaDateTimeConstants:ERROR",
"-Xep:JodaDurationWithMillis:ERROR",
@@ -311,7 +300,7 @@
"-Xep:MisusedDayOfYear:ERROR",
"-Xep:MisusedWeekYear:ERROR",
"-Xep:MixedDescriptors:ERROR",
- # "-Xep:MixedMutabilityReturnType:WARN",
+ "-Xep:MixedMutabilityReturnType:ERROR",
"-Xep:MockitoUsage:ERROR",
"-Xep:ModifiedButNotUsed:ERROR",
"-Xep:ModifyCollectionInEnhancedForLoop:ERROR",
@@ -321,13 +310,13 @@
"-Xep:MultipleUnaryOperatorsInMethodCall:ERROR",
"-Xep:MustBeClosedChecker:ERROR",
"-Xep:MutableConstantField:ERROR",
- # "-Xep:MutablePublicArray:WARN",
+ "-Xep:MutablePublicArray:ERROR",
"-Xep:NCopiesOfChar:ERROR",
"-Xep:NarrowingCompoundAssignment:ERROR",
"-Xep:NestedInstanceOfConditions:ERROR",
"-Xep:NonAtomicVolatileUpdate:ERROR",
"-Xep:NonCanonicalStaticImport:ERROR",
- # "-Xep:NonCanonicalType:WARN",
+ "-Xep:NonCanonicalType:ERROR",
"-Xep:NonFinalCompileTimeConstant:ERROR",
"-Xep:NonOverridingEquals:ERROR",
"-Xep:NonRuntimeAnnotation:ERROR",
@@ -364,7 +353,7 @@
"-Xep:PreconditionsInvalidPlaceholder:ERROR",
"-Xep:PrimitiveAtomicReference:ERROR",
"-Xep:PrivateSecurityContractProtoAccess:ERROR",
- # "-Xep:ProtectedMembersInFinalClass:WARN",
+ "-Xep:ProtectedMembersInFinalClass:ERROR",
"-Xep:ProtoBuilderReturnValueIgnored:ERROR",
"-Xep:ProtoDurationGetSecondsGetNano:ERROR",
"-Xep:ProtoFieldNullComparison:ERROR",
@@ -387,7 +376,7 @@
"-Xep:ReturnFromVoid:ERROR",
"-Xep:ReturnValueIgnored:ERROR",
"-Xep:RxReturnValueIgnored:ERROR",
- # "-Xep:SameNameButDifferent:WARN",
+ "-Xep:SameNameButDifferent:ERROR",
"-Xep:SelfAssignment:ERROR",
"-Xep:SelfComparison:ERROR",
"-Xep:SelfEquals:ERROR",
@@ -401,7 +390,7 @@
"-Xep:StreamToString:ERROR",
"-Xep:StringBuilderInitWithChar:ERROR",
"-Xep:StringEquality:ERROR",
- # "-Xep:StringSplitter:WARN",
+ "-Xep:StringSplitter:ERROR",
"-Xep:SubstringOfZero:ERROR",
"-Xep:SuppressWarningsDeprecated:ERROR",
"-Xep:SwigMemoryLeak:ERROR",
@@ -409,9 +398,9 @@
"-Xep:TemporalAccessorGetChronoField:ERROR",
"-Xep:TestParametersNotInitialized:ERROR",
"-Xep:TheoryButNoTheories:ERROR",
- # "-Xep:ThreadJoinLoop:WARN",
+ "-Xep:ThreadJoinLoop:ERROR",
"-Xep:ThreadLocalUsage:ERROR",
- # "-Xep:ThreadPriorityCheck:WARN",
+ "-Xep:ThreadPriorityCheck:ERROR",
"-Xep:ThreeLetterTimeZoneID:ERROR",
"-Xep:ThrowIfUncheckedKnownChecked:ERROR",
"-Xep:ThrowNull:ERROR",
@@ -430,14 +419,14 @@
"-Xep:TypeParameterShadowing:ERROR",
"-Xep:TypeParameterUnusedInFormals:ERROR",
"-Xep:URLEqualsHashCode:ERROR",
- # "-Xep:UndefinedEquals:WARN",
+ "-Xep:UndefinedEquals:ERROR",
"-Xep:UnescapedEntity:ERROR",
"-Xep:UnnecessaryAssignment:ERROR",
"-Xep:UnnecessaryCheckNotNull:ERROR",
- # "-Xep:UnnecessaryLambda:WARN",
+ "-Xep:UnnecessaryLambda:ERROR",
"-Xep:UnnecessaryMethodInvocationMatcher:ERROR",
"-Xep:UnnecessaryMethodReference:ERROR",
- # "-Xep:UnnecessaryParentheses:WARN",
+ "-Xep:UnnecessaryParentheses:ERROR",
"-Xep:UnnecessaryTypeArgument:ERROR",
"-Xep:UnrecognisedJavadocTag:ERROR",
"-Xep:UnsafeFinalization:ERROR",
diff --git a/tools/bzl/javadoc.bzl b/tools/bzl/javadoc.bzl
index 62b4010..3add025 100644
--- a/tools/bzl/javadoc.bzl
+++ b/tools/bzl/javadoc.bzl
@@ -61,14 +61,14 @@
command = " && ".join(cmd),
)
-_java_doc = rule(
+java_doc = rule(
attrs = {
"external_docs": attr.string_list(),
"libs": attr.label_list(allow_files = False),
"pkgs": attr.string_list(),
"title": attr.string(),
"_jdk": attr.label(
- default = Label("@bazel_tools//tools/jdk:current_host_java_runtime"),
+ default = Label("@bazel_tools//tools/jdk:current_java_runtime"),
allow_files = True,
providers = [java_common.JavaRuntimeInfo],
),
@@ -76,16 +76,3 @@
outputs = {"zip": "%{name}.zip"},
implementation = _impl,
)
-
-def java_doc(**kwargs):
- libs = kwargs.get("libs", [])
- libs = libs + select({
- "//:java11": [],
- "//:java_next": [],
- # TODO(davido): Remove this dependency, when Java 8 support is removed.
- # auto-value generates @javax.annotation.Generated annotation on generated
- # classes when Java 8 source compatibility level is used, but Java 11 and
- # later don't have this class any more.
- "//conditions:default": ["//lib:javax-annotation"],
- })
- _java_doc(**dict(kwargs, libs = libs))
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
index 3695e16..dec5f67 100644
--- a/tools/bzl/junit.bzl
+++ b/tools/bzl/junit.bzl
@@ -25,6 +25,7 @@
@RunWith(Suite.class)
@Suite.SuiteClasses({%s})
+@SuppressWarnings("DefaultPackage")
public class %s {}
"""
diff --git a/tools/bzl/license-map.py b/tools/bzl/license-map.py
index 43b172c..79285e6 100644
--- a/tools/bzl/license-map.py
+++ b/tools/bzl/license-map.py
@@ -170,7 +170,7 @@
Gerrit includes an SSH daemon (Apache SSHD), to support authenticated
uploads of changes directly from `git push` command line clients.
-Gerrit includes an SSH client (JSch), to support authenticated
+Gerrit includes an SSH client (Apache SSHD), to support authenticated
replication of changes to remote systems, such as for automatic
updates of mirror servers, or realtime backups.
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
index 2b473bc..43e9b3e 100644
--- a/tools/bzl/pkg_war.bzl
+++ b/tools/bzl/pkg_war.bzl
@@ -22,8 +22,6 @@
"//lib/bouncycastle:bcpkix",
"//lib/bouncycastle:bcprov",
"//lib/bouncycastle:bcpg",
- "//lib/log:impl-log4j",
- "//lib:jgit-ssh-jsch",
"//prolog:gerrit-prolog-common",
"//resources:log4j-config",
]
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index d445be2..9e515e5 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -21,6 +21,7 @@
provided_deps = [],
srcs = [],
resources = [],
+ resource_jars = [],
manifest_entries = [],
dir_name = None,
target_suffix = "",
@@ -35,8 +36,6 @@
**kwargs
)
- static_jars = []
-
if not dir_name:
dir_name = name
@@ -49,7 +48,7 @@
main_class = "Dummy",
runtime_deps = [
":%s__plugin" % name,
- ] + static_jars,
+ ] + resource_jars,
deploy_env = deploy_env,
visibility = ["//visibility:public"],
**kwargs
diff --git a/tools/deps.bzl b/tools/deps.bzl
new file mode 100644
index 0000000..2e89b9e
--- /dev/null
+++ b/tools/deps.bzl
@@ -0,0 +1,719 @@
+load("//tools/bzl:maven_jar.bzl", "GERRIT", "maven_jar")
+
+CAFFEINE_VERS = "2.8.5"
+ANTLR_VERS = "3.5.2"
+SLF4J_VERS = "1.7.26"
+COMMONMARK_VERS = "0.10.0"
+FLEXMARK_VERS = "0.50.42"
+GREENMAIL_VERS = "1.5.5"
+MAIL_VERS = "1.6.0"
+MIME4J_VERS = "0.8.1"
+OW2_VERS = "9.0"
+AUTO_VALUE_VERSION = "1.7.4"
+AUTO_VALUE_GSON_VERSION = "1.3.1"
+PROLOG_VERS = "1.4.4"
+PROLOG_REPO = GERRIT
+GITILES_VERS = "0.4-1"
+GITILES_REPO = GERRIT
+
+# When updating Bouncy Castle, also update it in bazlets.
+BC_VERS = "1.61"
+HTTPCOMP_VERS = "4.5.2"
+JETTY_VERS = "9.4.36.v20210114"
+BYTE_BUDDY_VERSION = "1.10.7"
+
+def java_dependencies():
+ maven_jar(
+ name = "java-runtime",
+ artifact = "org.antlr:antlr-runtime:" + ANTLR_VERS,
+ sha1 = "cd9cd41361c155f3af0f653009dcecb08d8b4afd",
+ )
+
+ maven_jar(
+ name = "stringtemplate",
+ artifact = "org.antlr:stringtemplate:4.0.2",
+ sha1 = "e28e09e2d44d60506a7bcb004d6c23ff35c6ac08",
+ )
+
+ maven_jar(
+ name = "org-antlr",
+ artifact = "org.antlr:antlr:" + ANTLR_VERS,
+ sha1 = "c4a65c950bfc3e7d04309c515b2177c00baf7764",
+ )
+
+ maven_jar(
+ name = "antlr27",
+ artifact = "antlr:antlr:2.7.7",
+ attach_source = False,
+ sha1 = "83cd2cd674a217ade95a4bb83a8a14f351f48bd0",
+ )
+
+ maven_jar(
+ name = "aopalliance",
+ artifact = "aopalliance:aopalliance:1.0",
+ sha1 = "0235ba8b489512805ac13a8f9ea77a1ca5ebe3e8",
+ )
+
+ maven_jar(
+ name = "javax_inject",
+ artifact = "javax.inject:javax.inject:1",
+ sha1 = "6975da39a7040257bd51d21a231b76c915872d38",
+ )
+
+ maven_jar(
+ name = "servlet-api",
+ artifact = "org.apache.tomcat:tomcat-servlet-api:8.5.23",
+ sha1 = "021a212688ec94fe77aff74ab34cc74f6f940e60",
+ )
+
+ # JGit's transitive dependencies
+ maven_jar(
+ name = "hamcrest",
+ artifact = "org.hamcrest:hamcrest:2.2",
+ sha1 = "1820c0968dba3a11a1b30669bb1f01978a91dedc",
+ )
+
+ maven_jar(
+ name = "javaewah",
+ artifact = "com.googlecode.javaewah:JavaEWAH:1.1.12",
+ attach_source = False,
+ sha1 = "9feecc2b24d6bc9ff865af8d082f192238a293eb",
+ )
+
+ maven_jar(
+ name = "gson",
+ artifact = "com.google.code.gson:gson:2.8.7",
+ sha1 = "69d9503ea0a40ee16f0bcdac7e3eaf83d0fa914a",
+ )
+
+ maven_jar(
+ name = "caffeine",
+ artifact = "com.github.ben-manes.caffeine:caffeine:" + CAFFEINE_VERS,
+ sha1 = "f0eafef6e1529a44e36549cd9d1fc06d3a57f384",
+ )
+
+ maven_jar(
+ name = "guava-failureaccess",
+ artifact = "com.google.guava:failureaccess:1.0.1",
+ sha1 = "1dcf1de382a0bf95a3d8b0849546c88bac1292c9",
+ )
+
+ maven_jar(
+ name = "juniversalchardet",
+ artifact = "com.github.albfernandez:juniversalchardet:2.0.0",
+ sha1 = "28c59f58f5adcc307604602e2aa89e2aca14c554",
+ )
+
+ maven_jar(
+ name = "log-api",
+ artifact = "org.slf4j:slf4j-api:" + SLF4J_VERS,
+ sha1 = "77100a62c2e6f04b53977b9f541044d7d722693d",
+ )
+
+ maven_jar(
+ name = "log-ext",
+ artifact = "org.slf4j:slf4j-ext:" + SLF4J_VERS,
+ sha1 = "31cdf122e000322e9efcb38913e9ab07825b17ef",
+ )
+
+ maven_jar(
+ name = "jcl-over-slf4j",
+ artifact = "org.slf4j:jcl-over-slf4j:" + SLF4J_VERS,
+ sha1 = "33fbc2d93de829fa5e263c5ce97f5eab8f57d53e",
+ )
+
+ maven_jar(
+ name = "log4j",
+ artifact = "log4j:log4j:1.2.17",
+ sha1 = "5af35056b4d257e4b64b9e8069c0746e8b08629f",
+ )
+
+ maven_jar(
+ name = "json-smart",
+ artifact = "net.minidev:json-smart:1.1.1",
+ sha1 = "24a2f903d25e004de30ac602c5b47f2d4e420a59",
+ )
+
+ maven_jar(
+ name = "args4j",
+ artifact = "args4j:args4j:2.33",
+ sha1 = "bd87a75374a6d6523de82fef51fc3cfe9baf9fc9",
+ )
+
+ maven_jar(
+ name = "commons-codec",
+ artifact = "commons-codec:commons-codec:1.10",
+ sha1 = "4b95f4897fa13f2cd904aee711aeafc0c5295cd8",
+ )
+
+ # When upgrading commons-compress, also upgrade tukaani-xz
+ maven_jar(
+ name = "commons-compress",
+ artifact = "org.apache.commons:commons-compress:1.20",
+ sha1 = "b8df472b31e1f17c232d2ad78ceb1c84e00c641b",
+ )
+
+ maven_jar(
+ name = "commons-lang",
+ artifact = "commons-lang:commons-lang:2.6",
+ sha1 = "0ce1edb914c94ebc388f086c6827e8bdeec71ac2",
+ )
+
+ maven_jar(
+ name = "commons-lang3",
+ artifact = "org.apache.commons:commons-lang3:3.8.1",
+ sha1 = "6505a72a097d9270f7a9e7bf42c4238283247755",
+ )
+
+ maven_jar(
+ name = "commons-text",
+ artifact = "org.apache.commons:commons-text:1.2",
+ sha1 = "74acdec7237f576c4803fff0c1008ab8a3808b2b",
+ )
+
+ maven_jar(
+ name = "commons-dbcp",
+ artifact = "commons-dbcp:commons-dbcp:1.4",
+ sha1 = "30be73c965cc990b153a100aaaaafcf239f82d39",
+ )
+
+ # Transitive dependency of commons-dbcp, do not update without
+ # also updating commons-dbcp
+ maven_jar(
+ name = "commons-pool",
+ artifact = "commons-pool:commons-pool:1.5.5",
+ sha1 = "7d8ffbdc47aa0c5a8afe5dc2aaf512f369f1d19b",
+ )
+
+ maven_jar(
+ name = "commons-net",
+ artifact = "commons-net:commons-net:3.6",
+ sha1 = "b71de00508dcb078d2b24b5fa7e538636de9b3da",
+ )
+
+ maven_jar(
+ name = "commons-validator",
+ artifact = "commons-validator:commons-validator:1.6",
+ sha1 = "e989d1e87cdd60575df0765ed5bac65c905d7908",
+ )
+
+ maven_jar(
+ name = "automaton",
+ artifact = "dk.brics:automaton:1.12-1",
+ sha1 = "959a0c62f9a5c2309e0ad0b0589c74d69e101241",
+ )
+
+ # commonmark must match the version used in Gitiles
+ maven_jar(
+ name = "commonmark",
+ artifact = "com.atlassian.commonmark:commonmark:" + COMMONMARK_VERS,
+ sha1 = "119cb7bedc3570d9ecb64ec69ab7686b5c20559b",
+ )
+
+ maven_jar(
+ name = "cm-autolink",
+ artifact = "com.atlassian.commonmark:commonmark-ext-autolink:" + COMMONMARK_VERS,
+ sha1 = "a6056a5efbd68f57d420bc51bbc54b28a5d3c56b",
+ )
+
+ maven_jar(
+ name = "gfm-strikethrough",
+ artifact = "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:" + COMMONMARK_VERS,
+ sha1 = "40837da951b421b545edddac57012e15fcc9e63c",
+ )
+
+ maven_jar(
+ name = "gfm-tables",
+ artifact = "com.atlassian.commonmark:commonmark-ext-gfm-tables:" + COMMONMARK_VERS,
+ sha1 = "c075db2a3301100cf70c7dced8ecf86b494458a2",
+ )
+
+ maven_jar(
+ name = "flexmark",
+ artifact = "com.vladsch.flexmark:flexmark:" + FLEXMARK_VERS,
+ sha1 = "ed537d7bc31883b008cc17d243a691c7efd12a72",
+ )
+
+ maven_jar(
+ name = "flexmark-ext-abbreviation",
+ artifact = "com.vladsch.flexmark:flexmark-ext-abbreviation:" + FLEXMARK_VERS,
+ sha1 = "dc27c3e7abbc8d2cfb154f41c68645c365bb9d22",
+ )
+
+ maven_jar(
+ name = "flexmark-ext-anchorlink",
+ artifact = "com.vladsch.flexmark:flexmark-ext-anchorlink:" + FLEXMARK_VERS,
+ sha1 = "6a8edb0165f695c9c19b7143a7fbd78c25c3b99c",
+ )
+
+ maven_jar(
+ name = "flexmark-ext-autolink",
+ artifact = "com.vladsch.flexmark:flexmark-ext-autolink:" + FLEXMARK_VERS,
+ sha1 = "5da7a4d009ea08ef2d8714cc73e54a992c6d2d9a",
+ )
+
+ maven_jar(
+ name = "flexmark-ext-definition",
+ artifact = "com.vladsch.flexmark:flexmark-ext-definition:" + FLEXMARK_VERS,
+ sha1 = "862d17812654624ed81ce8fc89c5ef819ff45f87",
+ )
+
+ maven_jar(
+ name = "flexmark-ext-emoji",
+ artifact = "com.vladsch.flexmark:flexmark-ext-emoji:" + FLEXMARK_VERS,
+ sha1 = "f0d7db64cb546798742b1ffc6db316a33f6acd76",
+ )
+
+ maven_jar(
+ name = "flexmark-ext-escaped-character",
+ artifact = "com.vladsch.flexmark:flexmark-ext-escaped-character:" + FLEXMARK_VERS,
+ sha1 = "6fd9ab77619df417df949721cb29c45914b326f8",
+ )
+
+ maven_jar(
+ name = "flexmark-ext-footnotes",
+ artifact = "com.vladsch.flexmark:flexmark-ext-footnotes:" + FLEXMARK_VERS,
+ sha1 = "e36bd69e43147cc6e19c3f55e4b27c0fc5a3d88c",
+ )
+
+ maven_jar(
+ name = "flexmark-ext-gfm-issues",
+ artifact = "com.vladsch.flexmark:flexmark-ext-gfm-issues:" + FLEXMARK_VERS,
+ sha1 = "5c825dd4e4fa4f7ccbe30dc92d7e35cdcb8a8c24",
+ )
+
+ maven_jar(
+ name = "flexmark-ext-gfm-strikethrough",
+ artifact = "com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:" + FLEXMARK_VERS,
+ sha1 = "3256735fd77e7228bf40f7888b4d3dc56787add4",
+ )
+
+ maven_jar(
+ name = "flexmark-ext-gfm-tables",
+ artifact = "com.vladsch.flexmark:flexmark-ext-gfm-tables:" + FLEXMARK_VERS,
+ sha1 = "62f0efcfb974756940ebe749fd4eb01323babc29",
+ )
+
+ maven_jar(
+ name = "flexmark-ext-gfm-tasklist",
+ artifact = "com.vladsch.flexmark:flexmark-ext-gfm-tasklist:" + FLEXMARK_VERS,
+ sha1 = "76d4971ad9ce02f0e70351ab6bd06ad8e405e40d",
+ )
+
+ maven_jar(
+ name = "flexmark-ext-gfm-users",
+ artifact = "com.vladsch.flexmark:flexmark-ext-gfm-users:" + FLEXMARK_VERS,
+ sha1 = "7b0fc7e42e4da508da167fcf8e1cbf9ba7e21147",
+ )
+
+ maven_jar(
+ name = "flexmark-ext-ins",
+ artifact = "com.vladsch.flexmark:flexmark-ext-ins:" + FLEXMARK_VERS,
+ sha1 = "9e51809867b9c4db0fb1c29599b4574e3d2a78e9",
+ )
+
+ maven_jar(
+ name = "flexmark-ext-jekyll-front-matter",
+ artifact = "com.vladsch.flexmark:flexmark-ext-jekyll-front-matter:" + FLEXMARK_VERS,
+ sha1 = "44eb6dbb33b3831d3b40af938ddcd99c9c16a654",
+ )
+
+ maven_jar(
+ name = "flexmark-ext-superscript",
+ artifact = "com.vladsch.flexmark:flexmark-ext-superscript:" + FLEXMARK_VERS,
+ sha1 = "35815b8cb91000344d1fe5df21cacde8553d2994",
+ )
+
+ maven_jar(
+ name = "flexmark-ext-tables",
+ artifact = "com.vladsch.flexmark:flexmark-ext-tables:" + FLEXMARK_VERS,
+ sha1 = "f6768e98c7210b79d5e8bab76fff27eec6db51e6",
+ )
+
+ maven_jar(
+ name = "flexmark-ext-toc",
+ artifact = "com.vladsch.flexmark:flexmark-ext-toc:" + FLEXMARK_VERS,
+ sha1 = "1968d038fc6c8156f244f5a7eecb34e7e2f33705",
+ )
+
+ maven_jar(
+ name = "flexmark-ext-typographic",
+ artifact = "com.vladsch.flexmark:flexmark-ext-typographic:" + FLEXMARK_VERS,
+ sha1 = "6549b9862b61c4434a855a733237103df9162849",
+ )
+
+ maven_jar(
+ name = "flexmark-ext-wikilink",
+ artifact = "com.vladsch.flexmark:flexmark-ext-wikilink:" + FLEXMARK_VERS,
+ sha1 = "e105b09dd35aab6e6f5c54dfe062ee59bd6f786a",
+ )
+
+ maven_jar(
+ name = "flexmark-ext-yaml-front-matter",
+ artifact = "com.vladsch.flexmark:flexmark-ext-yaml-front-matter:" + FLEXMARK_VERS,
+ sha1 = "b2d3a1e7f3985841062e8d3203617e29c6c21b52",
+ )
+
+ maven_jar(
+ name = "flexmark-formatter",
+ artifact = "com.vladsch.flexmark:flexmark-formatter:" + FLEXMARK_VERS,
+ sha1 = "a50c6cb10f6d623fc4354a572c583de1372d217f",
+ )
+
+ maven_jar(
+ name = "flexmark-html-parser",
+ artifact = "com.vladsch.flexmark:flexmark-html-parser:" + FLEXMARK_VERS,
+ sha1 = "46c075f30017e131c1ada8538f1d8eacf652b044",
+ )
+
+ maven_jar(
+ name = "flexmark-profile-pegdown",
+ artifact = "com.vladsch.flexmark:flexmark-profile-pegdown:" + FLEXMARK_VERS,
+ sha1 = "d9aafd47629959cbeddd731f327ae090fc92b60f",
+ )
+
+ maven_jar(
+ name = "flexmark-util",
+ artifact = "com.vladsch.flexmark:flexmark-util:" + FLEXMARK_VERS,
+ sha1 = "417a9821d5d80ddacbfecadc6843ae7b259d5112",
+ )
+
+ # Transitive dependency of flexmark and gitiles
+ maven_jar(
+ name = "autolink",
+ artifact = "org.nibor.autolink:autolink:0.7.0",
+ sha1 = "649f9f13422cf50c926febe6035662ae25dc89b2",
+ )
+
+ maven_jar(
+ name = "greenmail",
+ artifact = "com.icegreen:greenmail:" + GREENMAIL_VERS,
+ sha1 = "9ea96384ad2cb8118c22f493b529eb72c212691c",
+ )
+
+ maven_jar(
+ name = "mail",
+ artifact = "com.sun.mail:javax.mail:" + MAIL_VERS,
+ sha1 = "a055c648842c4954c1f7db7254f45d9ad565e278",
+ )
+
+ maven_jar(
+ name = "mime4j-core",
+ artifact = "org.apache.james:apache-mime4j-core:" + MIME4J_VERS,
+ sha1 = "c62dfe18a3b827a2c626ade0ffba44562ddf3f61",
+ )
+
+ maven_jar(
+ name = "mime4j-dom",
+ artifact = "org.apache.james:apache-mime4j-dom:" + MIME4J_VERS,
+ sha1 = "f2d653c617004193f3350330d907f77b60c88c56",
+ )
+
+ maven_jar(
+ name = "jsoup",
+ artifact = "org.jsoup:jsoup:1.9.2",
+ sha1 = "5e3bda828a80c7a21dfbe2308d1755759c2fd7b4",
+ )
+
+ maven_jar(
+ name = "ow2-asm",
+ artifact = "org.ow2.asm:asm:" + OW2_VERS,
+ sha1 = "af582ff60bc567c42d931500c3fdc20e0141ddf9",
+ )
+
+ maven_jar(
+ name = "ow2-asm-analysis",
+ artifact = "org.ow2.asm:asm-analysis:" + OW2_VERS,
+ sha1 = "4630afefbb43939c739445dde0af1a5729a0fb4e",
+ )
+
+ maven_jar(
+ name = "ow2-asm-commons",
+ artifact = "org.ow2.asm:asm-commons:" + OW2_VERS,
+ sha1 = "5a34a3a9ac44f362f35d1b27932380b0031a3334",
+ )
+
+ maven_jar(
+ name = "ow2-asm-tree",
+ artifact = "org.ow2.asm:asm-tree:" + OW2_VERS,
+ sha1 = "9df939f25c556b0c7efe00701d47e77a49837f24",
+ )
+
+ maven_jar(
+ name = "ow2-asm-util",
+ artifact = "org.ow2.asm:asm-util:" + OW2_VERS,
+ sha1 = "7c059a94ab5eed3347bf954e27fab58e52968848",
+ )
+
+ maven_jar(
+ name = "auto-value",
+ artifact = "com.google.auto.value:auto-value:" + AUTO_VALUE_VERSION,
+ sha1 = "6b126cb218af768339e4d6e95a9b0ae41f74e73d",
+ )
+
+ maven_jar(
+ name = "auto-value-annotations",
+ artifact = "com.google.auto.value:auto-value-annotations:" + AUTO_VALUE_VERSION,
+ sha1 = "eff48ed53995db2dadf0456426cc1f8700136f86",
+ )
+
+ maven_jar(
+ name = "auto-value-gson-runtime",
+ artifact = "com.ryanharter.auto.value:auto-value-gson-runtime:" + AUTO_VALUE_GSON_VERSION,
+ sha1 = "addda2ae6cce9f855788274df5de55dde4de7b71",
+ )
+
+ maven_jar(
+ name = "auto-value-gson-extension",
+ artifact = "com.ryanharter.auto.value:auto-value-gson-extension:" + AUTO_VALUE_GSON_VERSION,
+ sha1 = "0c4c01a3e10e5b10df2e5f5697efa4bb3f453ac1",
+ )
+
+ maven_jar(
+ name = "auto-value-gson-factory",
+ artifact = "com.ryanharter.auto.value:auto-value-gson-factory:" + AUTO_VALUE_GSON_VERSION,
+ sha1 = "9ed8d79144ee8d60cc94cc11f847b5ed8ee9f19c",
+ )
+
+ maven_jar(
+ name = "javapoet",
+ artifact = "com.squareup:javapoet:1.13.0",
+ sha1 = "d6562d385049f35eb50403fa86bb11cce76b866a",
+ )
+
+ maven_jar(
+ name = "autotransient",
+ artifact = "io.sweers.autotransient:autotransient:1.0.0",
+ sha1 = "38b1c630b8e76560221622289f37be40105abb3d",
+ )
+
+ maven_jar(
+ name = "mime-util",
+ artifact = "eu.medsea.mimeutil:mime-util:2.1.3",
+ attach_source = False,
+ sha1 = "0c9cfae15c74f62491d4f28def0dff1dabe52a47",
+ )
+
+ maven_jar(
+ name = "prolog-runtime",
+ artifact = "com.googlecode.prolog-cafe:prolog-runtime:" + PROLOG_VERS,
+ attach_source = False,
+ repository = PROLOG_REPO,
+ sha1 = "e9a364f4233481cce63239e8e68a6190c8f58acd",
+ )
+
+ maven_jar(
+ name = "prolog-compiler",
+ artifact = "com.googlecode.prolog-cafe:prolog-compiler:" + PROLOG_VERS,
+ attach_source = False,
+ repository = PROLOG_REPO,
+ sha1 = "570295026f6aa7b905e423d107cb2e081eecdc04",
+ )
+
+ maven_jar(
+ name = "prolog-io",
+ artifact = "com.googlecode.prolog-cafe:prolog-io:" + PROLOG_VERS,
+ attach_source = False,
+ repository = PROLOG_REPO,
+ sha1 = "1f25c4e27d22bdbc31481ee0c962a2a2853e4428",
+ )
+
+ maven_jar(
+ name = "cafeteria",
+ artifact = "com.googlecode.prolog-cafe:prolog-cafeteria:" + PROLOG_VERS,
+ attach_source = False,
+ repository = PROLOG_REPO,
+ sha1 = "0e6c2deeaf5054815a561cbd663566fd59b56c6c",
+ )
+
+ maven_jar(
+ name = "guava-retrying",
+ artifact = "com.github.rholder:guava-retrying:2.0.0",
+ sha1 = "974bc0a04a11cc4806f7c20a34703bd23c34e7f4",
+ )
+
+ maven_jar(
+ name = "jsr305",
+ artifact = "com.google.code.findbugs:jsr305:3.0.1",
+ sha1 = "f7be08ec23c21485b9b5a1cf1654c2ec8c58168d",
+ )
+
+ maven_jar(
+ name = "blame-cache",
+ artifact = "com.google.gitiles:blame-cache:" + GITILES_VERS,
+ attach_source = False,
+ repository = GITILES_REPO,
+ sha1 = "0df80c6b8822147e1f116fd7804b8a0de544f402",
+ )
+
+ maven_jar(
+ name = "gitiles-servlet",
+ artifact = "com.google.gitiles:gitiles-servlet:" + GITILES_VERS,
+ repository = GITILES_REPO,
+ sha1 = "60870897d22b840e65623fd024eabd9cc9706ebe",
+ )
+
+ # prettify must match the version used in Gitiles
+ maven_jar(
+ name = "prettify",
+ artifact = "com.github.twalcari:java-prettify:1.2.2",
+ sha1 = "b8ba1c1eb8b2e45cfd465d01218c6060e887572e",
+ )
+
+ maven_jar(
+ name = "html-types",
+ artifact = "com.google.common.html.types:types:1.0.8",
+ sha1 = "9e9cf7bc4b2a60efeb5f5581fe46d17c068e0777",
+ )
+
+ maven_jar(
+ name = "icu4j",
+ artifact = "com.ibm.icu:icu4j:57.1",
+ sha1 = "198ea005f41219f038f4291f0b0e9f3259730e92",
+ )
+
+ maven_jar(
+ name = "bcprov",
+ artifact = "org.bouncycastle:bcprov-jdk15on:" + BC_VERS,
+ sha1 = "00df4b474e71be02c1349c3292d98886f888d1f7",
+ )
+
+ maven_jar(
+ name = "bcpg",
+ artifact = "org.bouncycastle:bcpg-jdk15on:" + BC_VERS,
+ sha1 = "422656435514ab8a28752b117d5d2646660a0ace",
+ )
+
+ maven_jar(
+ name = "bcpkix",
+ artifact = "org.bouncycastle:bcpkix-jdk15on:" + BC_VERS,
+ sha1 = "89bb3aa5b98b48e584eee2a7401b7682a46779b4",
+ )
+
+ maven_jar(
+ name = "h2",
+ artifact = "com.h2database:h2:1.3.176",
+ sha1 = "fd369423346b2f1525c413e33f8cf95b09c92cbd",
+ )
+
+ maven_jar(
+ name = "fluent-hc",
+ artifact = "org.apache.httpcomponents:fluent-hc:" + HTTPCOMP_VERS,
+ sha1 = "7bfdfa49de6d720ad3c8cedb6a5238eec564dfed",
+ )
+
+ maven_jar(
+ name = "httpclient",
+ artifact = "org.apache.httpcomponents:httpclient:" + HTTPCOMP_VERS,
+ sha1 = "733db77aa8d9b2d68015189df76ab06304406e50",
+ )
+
+ maven_jar(
+ name = "httpcore",
+ artifact = "org.apache.httpcomponents:httpcore:4.4.4",
+ sha1 = "b31526a230871fbe285fbcbe2813f9c0839ae9b0",
+ )
+
+ # Test-only dependencies below.
+ maven_jar(
+ name = "junit",
+ artifact = "junit:junit:4.12",
+ sha1 = "2973d150c0dc1fefe998f834810d68f278ea58ec",
+ )
+
+ maven_jar(
+ name = "hamcrest-core",
+ artifact = "org.hamcrest:hamcrest-core:1.3",
+ sha1 = "42a25dc3219429f0e5d060061f71acb49bf010a0",
+ )
+
+ maven_jar(
+ name = "diffutils",
+ artifact = "com.googlecode.java-diff-utils:diffutils:1.3.0",
+ sha1 = "7e060dd5b19431e6d198e91ff670644372f60fbd",
+ )
+
+ maven_jar(
+ name = "jetty-servlet",
+ artifact = "org.eclipse.jetty:jetty-servlet:" + JETTY_VERS,
+ sha1 = "b189e52a5ee55ae172e4e99e29c5c314f5daf4b9",
+ )
+
+ maven_jar(
+ name = "jetty-security",
+ artifact = "org.eclipse.jetty:jetty-security:" + JETTY_VERS,
+ sha1 = "42030d6ed7dfc0f75818cde0adcf738efc477574",
+ )
+
+ maven_jar(
+ name = "jetty-server",
+ artifact = "org.eclipse.jetty:jetty-server:" + JETTY_VERS,
+ sha1 = "88a7d342974aadca658e7386e8d0fcc5c0788f41",
+ )
+
+ maven_jar(
+ name = "jetty-jmx",
+ artifact = "org.eclipse.jetty:jetty-jmx:" + JETTY_VERS,
+ sha1 = "bb3847eabe085832aeaedd30e872b40931632e54",
+ )
+
+ maven_jar(
+ name = "jetty-http",
+ artifact = "org.eclipse.jetty:jetty-http:" + JETTY_VERS,
+ sha1 = "1eee89a55e04ff94df0f85d95200fc48acb43d86",
+ )
+
+ maven_jar(
+ name = "jetty-io",
+ artifact = "org.eclipse.jetty:jetty-io:" + JETTY_VERS,
+ sha1 = "84a8faf9031eb45a5a2ddb7681e22c483d81ab3a",
+ )
+
+ maven_jar(
+ name = "jetty-util",
+ artifact = "org.eclipse.jetty:jetty-util:" + JETTY_VERS,
+ sha1 = "925257fbcca6b501a25252c7447dbedb021f7404",
+ )
+
+ maven_jar(
+ name = "jetty-util-ajax",
+ artifact = "org.eclipse.jetty:jetty-util-ajax:" + JETTY_VERS,
+ sha1 = "2f478130c21787073facb64d7242e06f94980c60",
+ src_sha1 = "7153d7ca38878d971fd90992c303bb7719ba7a21",
+ )
+
+ maven_jar(
+ name = "asciidoctor",
+ artifact = "org.asciidoctor:asciidoctorj:1.5.7",
+ sha1 = "8e8c1d8fc6144405700dd8df3b177f2801ac5987",
+ )
+
+ maven_jar(
+ name = "javax-activation",
+ artifact = "javax.activation:activation:1.1.1",
+ sha1 = "485de3a253e23f645037828c07f1d7f1af40763a",
+ )
+
+ maven_jar(
+ name = "mockito",
+ artifact = "org.mockito:mockito-core:3.3.3",
+ sha1 = "4878395d4e63173f3825e17e5e0690e8054445f1",
+ )
+
+ maven_jar(
+ name = "bytebuddy",
+ artifact = "net.bytebuddy:byte-buddy:" + BYTE_BUDDY_VERSION,
+ sha1 = "1eefb7dd1b032b33c773ca0a17d5cc9e6b56ea1a",
+ )
+
+ maven_jar(
+ name = "bytebuddy-agent",
+ artifact = "net.bytebuddy:byte-buddy-agent:" + BYTE_BUDDY_VERSION,
+ sha1 = "c472fad33f617228601172682aa64f8b78508045",
+ )
+
+ maven_jar(
+ name = "objenesis",
+ artifact = "org.objenesis:objenesis:3.0.1",
+ sha1 = "11cfac598df9dc48bb9ed9357ed04212694b7808",
+ )
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index ef20ace..d574ecf 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -49,9 +49,7 @@
opts.add_argument('-b', '--batch', action='store_true',
dest='batch', help='Bazel batch option')
opts.add_argument('-j', '--java', action='store',
- dest='java', help='Legacy Java 1.8 or post Java 11')
-opts.add_argument('-e', '--edge_java', action='store',
- dest='edge_java', help='Post Java 11 support (14|...)')
+ dest='java', help='Post Java 11')
opts.add_argument('--bazel',
help=('name of the bazel executable. Defaults to using'
' bazelisk if found, or bazel if bazelisk is not'
@@ -85,7 +83,6 @@
batch_option = '--batch' if args.batch else None
custom_java = args.java
-edge_java = args.edge_java
bazel_exe = find_bazel()
@@ -98,13 +95,9 @@
if arg == "build":
build = True
cmd.append(arg)
- if custom_java == '1.8':
- cmd.append('--java_toolchain=//tools:error_prone_warnings_toolchain')
- elif custom_java and not edge_java:
+ if custom_java:
cmd.append('--host_java_toolchain=@bazel_tools//tools/jdk:toolchain_java%s' % custom_java)
cmd.append('--java_toolchain=@bazel_tools//tools/jdk:toolchain_java%s' % custom_java)
- if edge_java and build:
- cmd.append(edge_java)
return cmd
@@ -186,8 +179,6 @@
classpathentry('src', 'modules/jgit/org.eclipse.jgit.http.server/src')
classpathentry('src', 'modules/jgit/org.eclipse.jgit.http.server/resources')
classpathentry('src', 'modules/jgit/org.eclipse.jgit.junit/src')
- classpathentry('src', 'modules/jgit/org.eclipse.jgit.ssh.jsch/src')
- classpathentry('src', 'modules/jgit/org.eclipse.jgit.ssh.jsch/resources')
classpathentry('src', 'modules/jgit/org.eclipse.jgit.ssh.apache/src')
classpathentry('src', 'modules/jgit/org.eclipse.jgit.ssh.apache/resources')
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index 7fc0b01..0ae0319 100644
--- a/tools/maven/gerrit-acceptance-framework_pom.xml
+++ b/tools/maven/gerrit-acceptance-framework_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-acceptance-framework</artifactId>
- <version>3.5.1-SNAPSHOT</version>
+ <version>3.6.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Acceptance Test Framework</name>
<description>Framework for Gerrit's acceptance tests</description>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index 9db1d06..0488183 100644
--- a/tools/maven/gerrit-extension-api_pom.xml
+++ b/tools/maven/gerrit-extension-api_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-extension-api</artifactId>
- <version>3.5.1-SNAPSHOT</version>
+ <version>3.6.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Extension API</name>
<description>API for Gerrit Extensions</description>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index 833ed51..ae4adf3 100644
--- a/tools/maven/gerrit-plugin-api_pom.xml
+++ b/tools/maven/gerrit-plugin-api_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-plugin-api</artifactId>
- <version>3.5.1-SNAPSHOT</version>
+ <version>3.6.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Plugin API</name>
<description>API for Gerrit Plugins</description>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml
index 93adc12..1a9cd6e 100644
--- a/tools/maven/gerrit-war_pom.xml
+++ b/tools/maven/gerrit-war_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-war</artifactId>
- <version>3.5.1-SNAPSHOT</version>
+ <version>3.6.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>Gerrit Code Review - WAR</name>
<description>Gerrit WAR</description>
diff --git a/tools/node_tools/launchpad.patch b/tools/node_tools/launchpad.patch
deleted file mode 100644
index 565494b..0000000
--- a/tools/node_tools/launchpad.patch
+++ /dev/null
@@ -1,240 +0,0 @@
-From d430b5d912bebe87529b887f408ee55c82a0e003 Mon Sep 17 00:00:00 2001
-From: Michele Romano <33063403+Mik317@users.noreply.github.com>
-Date: Fri, 26 Jun 2020 20:16:47 +0200
-Subject: [PATCH 1/7] Update version.js
-
----
- lib/local/version.js | 15 ++++++++++++---
- 1 file changed, 12 insertions(+), 3 deletions(-)
-
-diff --git a/tools/node_tools/node_modules/launchpad/lib/local/version.js b/tools/node_tools/node_modules/launchpad/lib/g/local/version.js
-index 0110a74..2c02bef 100644
---- a/tools/node_tools/node_modules/launchpad/lib/local/version.js
-+++ b/tools/node_tools/node_modules/launchpad/lib/local/version.js
-@@ -6,6 +6,15 @@ var plist = require('plist');
- var utils = require('./utils');
- var debug = require('debug')('launchpad:local:version');
-
-+var validPath = function (filename){
-+ var filter = /[`!@#$%^&*()_+\-=\[\]{};':"\\|,<>\/?~]/;
-+ if (filter.test(filename)){
-+ console.log('\nInvalid characters inside the path to the browser\n');
-+ return
-+ }
-+ return filename;
-+}
-+
- module.exports = function(browser) {
- if (!browser || !browser.path) {
- return Q(null);
-@@ -18,7 +27,7 @@ module.exports = function(browser) {
-
- debug('Retrieving version for windows executable', command);
- // Can't use Q.nfcall here unfortunately because of non 0 exit code
-- exec(command, function(error, stdout) {
-+ exec(command.split(' ')[0], command.split(' ').slice(1), function(error, stdout) {
- var regex = /ProductVersion:\s*(.*)/;
- // ShowVer.exe returns a non zero status code even if it works
- if (typeof stdout === 'string' && regex.test(stdout)) {
-@@ -47,8 +56,8 @@ module.exports = function(browser) {
- }
-
- // Try executing <browser> --version (everything else)
-- return Q.nfcall(exec, browser.path + ' --version').then(function(stdout) {
-- debug('Ran ' + browser.path + ' --version', stdout);
-+ return Q.nfcall(exec, validPath(browser.path) + ' --version').then(function(stdout) {
-+ debug('Ran ' + validPath(browser.path) + ' --version', stdout);
- var version = utils.getStdout(stdout);
- if (version) {
- browser.version = version;
-
-From 09ce4fab2fd53cab893ceaa3b4d7f997af9b41d8 Mon Sep 17 00:00:00 2001
-From: Michele Romano <33063403+Mik317@users.noreply.github.com>
-Date: Fri, 26 Jun 2020 20:18:35 +0200
-Subject: [PATCH 2/7] Update instance.js
-
----
- lib/local/instance.js | 11 +++++++++--
- 1 file changed, 9 insertions(+), 2 deletions(-)
-
-diff --git a/tools/node_tools/node_modules/launchpad/lib/local/instance.js b/tools/node_tools/node_modules/launchpad/lib/g/local/instance.js
-index 484a866..b49990f 100644
---- a/tools/node_tools/node_modules/launchpad/lib/local/instance.js
-+++ b/tools/node_tools/node_modules/launchpad/lib/local/instance.js
-@@ -5,8 +5,15 @@ var EventEmitter = require('events').EventEmitter;
- var debug = require('debug')('launchpad:local:instance');
- var rimraf = require('rimraf');
-
-+var safe = function (str) {
-+ // Avoid quotes makes impossible escape the `multi command` scenario
-+ return str.replace(/['"]+/g, '');
-+}
-+
- var getProcessId = function (name, callback) {
-
-+ name = safe(name);
-+
- var commands = {
- darwin: "ps -clx | grep '" + name + "$' | awk '{print $2}' | head -1",
- linux: "ps -ax | grep '" + name + "$' | awk '{print $2}' | head -1",
-@@ -90,11 +97,11 @@ Instance.prototype.stop = function (callback) {
- } catch (error) {}
- } else {
- if (this.options.command.indexOf('open') === 0) {
-- command = 'osascript -e \'tell application "' + self.options.process + '" to quit\'';
-+ command = 'osascript -e \'tell application "' + safe(self.options.process) + '" to quit\'';
- debug('Executing shutdown AppleScript', command);
- exec(command);
- } else if (process.platform === 'win32') {
-- command = 'taskkill /IM ' + (this.options.imageName || path.basename(this.cmd));
-+ command = 'taskkill /IM "' + safe(this.options.imageName || path.basename(this.cmd)) + '"';
- debug('Executing shutdown taskkil', command);
- exec(command).once('exit', function(data) {
- self.emit('stop', data);
-
-From d3993fce090ed6ef378c1f0594eff18d125dad1e Mon Sep 17 00:00:00 2001
-From: Michele Romano <33063403+Mik317@users.noreply.github.com>
-Date: Fri, 26 Jun 2020 20:19:17 +0200
-Subject: [PATCH 3/7] Update version.js
-
----
- lib/local/version.js | 1 +
- 1 file changed, 1 insertion(+)
-
-diff --git a/tools/node_tools/node_modules/launchpad/lib/local/version.js b/tools/node_tools/node_modules/launchpad/lib/g/local/version.js
-index 2c02bef..5eac082 100644
---- a/tools/node_tools/node_modules/launchpad/lib/local/version.js
-+++ b/tools/node_tools/node_modules/launchpad/lib/local/version.js
-@@ -6,6 +6,7 @@ var plist = require('plist');
- var utils = require('./utils');
- var debug = require('debug')('launchpad:local:version');
-
-+// Validate paths supplied by the user in order to avoid "arbitrary command execution"
- var validPath = function (filename){
- var filter = /[`!@#$%^&*()_+\-=\[\]{};':"\\|,<>\/?~]/;
- if (filter.test(filename)){
-
-From abf3dbcc79e6b338338594ab2dbef834550e8f65 Mon Sep 17 00:00:00 2001
-From: Michele Romano <33063403+Mik317@users.noreply.github.com>
-Date: Mon, 29 Jun 2020 13:32:50 +0200
-Subject: [PATCH 4/7] Update instance.js
-
----
- lib/local/instance.js | 10 +++++++---
- 1 file changed, 7 insertions(+), 3 deletions(-)
-
-diff --git a/tools/node_tools/node_modules/launchpad/lib/local/instance.js b/tools/node_tools/node_modules/launchpad/lib/g/local/instance.js
-index b49990f..9375d1f 100644
---- a/tools/node_tools/node_modules/launchpad/lib/local/instance.js
-+++ b/tools/node_tools/node_modules/launchpad/lib/local/instance.js
-@@ -1,6 +1,7 @@
- var path = require('path');
- var spawn = require("child_process").spawn;
- var exec = require("child_process").exec;
-+var execFile = require("child_process").execFile;
- var EventEmitter = require('events').EventEmitter;
- var debug = require('debug')('launchpad:local:instance');
- var rimraf = require('rimraf');
-@@ -99,11 +100,14 @@ Instance.prototype.stop = function (callback) {
- if (this.options.command.indexOf('open') === 0) {
- command = 'osascript -e \'tell application "' + safe(self.options.process) + '" to quit\'';
- debug('Executing shutdown AppleScript', command);
-- exec(command);
-+ command = command.split(' ');
-+ execFile(command[0], command.slice(1));
- } else if (process.platform === 'win32') {
-- command = 'taskkill /IM "' + safe(this.options.imageName || path.basename(this.cmd)) + '"';
-+ //Adding `"` wasn't safe/functional on Win systems
-+ command = 'taskkill /IM ' + (this.options.imageName || path.basename(this.cmd);
- debug('Executing shutdown taskkil', command);
-- exec(command).once('exit', function(data) {
-+ command = command.split(' ');
-+ execFile(command[0], command.slice(1)).once('exit', function(data) {
- self.emit('stop', data);
- });
- } else {
-
-From 68518b274c9351f799d41ce85f23499ca4a785e9 Mon Sep 17 00:00:00 2001
-From: Michele Romano <33063403+Mik317@users.noreply.github.com>
-Date: Tue, 30 Jun 2020 00:01:31 +0200
-Subject: [PATCH 5/7] Update instance.js
-
----
- lib/local/instance.js | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/tools/node_tools/node_modules/launchpad/lib/local/instance.js b/tools/node_tools/node_modules/launchpad/lib/g/local/instance.js
-index 9375d1f..f157dd4 100644
---- a/tools/node_tools/node_modules/launchpad/lib/local/instance.js
-+++ b/tools/node_tools/node_modules/launchpad/lib/local/instance.js
-@@ -104,7 +104,7 @@ Instance.prototype.stop = function (callback) {
- execFile(command[0], command.slice(1));
- } else if (process.platform === 'win32') {
- //Adding `"` wasn't safe/functional on Win systems
-- command = 'taskkill /IM ' + (this.options.imageName || path.basename(this.cmd);
-+ command = 'taskkill /IM ' + (this.options.imageName || path.basename(this.cmd));
- debug('Executing shutdown taskkil', command);
- command = command.split(' ');
- execFile(command[0], command.slice(1)).once('exit', function(data) {
-
-From e711d07d40d39162ea4bdb1ed344c58f92bfa10b Mon Sep 17 00:00:00 2001
-From: Michele Romano <33063403+Mik317@users.noreply.github.com>
-Date: Fri, 3 Jul 2020 12:30:31 +0200
-Subject: [PATCH 6/7] Update version.js
-
----
- lib/local/version.js | 5 +++--
- 1 file changed, 3 insertions(+), 2 deletions(-)
-
-diff --git a/tools/node_tools/node_modules/launchpad/lib/local/version.js b/tools/node_tools/node_modules/launchpad/lib/g/local/version.js
-index 5eac082..d1403a0 100644
---- a/tools/node_tools/node_modules/launchpad/lib/local/version.js
-+++ b/tools/node_tools/node_modules/launchpad/lib/local/version.js
-@@ -1,5 +1,6 @@
- var fs = require('fs');
- var exec = require('child_process').exec;
-+var execFile = require('child_process').execFile;
- var Q = require('q');
- var path = require('path');
- var plist = require('plist');
-@@ -8,7 +9,7 @@ var debug = require('debug')('launchpad:local:version');
-
- // Validate paths supplied by the user in order to avoid "arbitrary command execution"
- var validPath = function (filename){
-- var filter = /[`!@#$%^&*()_+\-=\[\]{};':"\\|,<>\/?~]/;
-+ var filter = /[`!@#$%^&*()_+\-=\[\]{};':"|,<>?~]/;
- if (filter.test(filename)){
- console.log('\nInvalid characters inside the path to the browser\n');
- return
-@@ -28,7 +29,7 @@ module.exports = function(browser) {
-
- debug('Retrieving version for windows executable', command);
- // Can't use Q.nfcall here unfortunately because of non 0 exit code
-- exec(command.split(' ')[0], command.split(' ').slice(1), function(error, stdout) {
-+ execFile(command.split(' ')[0], command.split(' ').slice(1), function(error, stdout) {
- var regex = /ProductVersion:\s*(.*)/;
- // ShowVer.exe returns a non zero status code even if it works
- if (typeof stdout === 'string' && regex.test(stdout)) {
-
-From a3ff1804f0aacfb4fa20dad1312427b81280bb3e Mon Sep 17 00:00:00 2001
-From: Michele Romano <33063403+Mik317@users.noreply.github.com>
-Date: Fri, 3 Jul 2020 12:31:31 +0200
-Subject: [PATCH 7/7] Update version.js
-
----
- lib/local/version.js | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/tools/node_tools/node_modules/launchpad/lib/local/version.js b/tools/node_tools/node_modules/launchpad/lib/g/local/version.js
-index d1403a0..d937be4 100644
---- a/tools/node_tools/node_modules/launchpad/lib/local/version.js
-+++ b/tools/node_tools/node_modules/launchpad/lib/local/version.js
-@@ -9,7 +9,7 @@ var debug = require('debug')('launchpad:local:version');
-
- // Validate paths supplied by the user in order to avoid "arbitrary command execution"
- var validPath = function (filename){
-- var filter = /[`!@#$%^&*()_+\-=\[\]{};':"|,<>?~]/;
-+ var filter = /[`!@#$%^&*()_+\-=\[\]{};'"|,<>?~]/;
- if (filter.test(filename)){
- console.log('\nInvalid characters inside the path to the browser\n');
- return
diff --git a/tools/node_tools/package.json b/tools/node_tools/package.json
index 7ee64df..33f9234 100644
--- a/tools/node_tools/package.json
+++ b/tools/node_tools/package.json
@@ -19,12 +19,11 @@
"typescript": "4.3.2"
},
"devDependencies": {},
- "scripts": {
- "postinstall": "(git apply --reverse --ignore-whitespace launchpad.patch || true) && git apply --ignore-whitespace launchpad.patch"
- },
"license": "Apache-2.0",
"private": true,
"resolutions": {
- "lodash": "4.17.21"
+ "lodash": "4.17.21",
+ "wct-local": "2.1.6",
+ "launchpad": "git+https://github.com/418sec/launchpad.git#de5aca11dc16a8e530195281c77614bdbb08e7be"
}
-}
+}
\ No newline at end of file
diff --git a/tools/node_tools/yarn.lock b/tools/node_tools/yarn.lock
index 6525c41..6e146ea 100644
--- a/tools/node_tools/yarn.lock
+++ b/tools/node_tools/yarn.lock
@@ -5341,10 +5341,9 @@
dependencies:
package-json "^4.0.0"
-launchpad@^0.7.0:
+"launchpad@git+https://github.com/418sec/launchpad.git#de5aca11dc16a8e530195281c77614bdbb08e7be", "launchpad@git://github.com/418sec/launchpad.git#de5aca11dc16a8e530195281c77614bdbb08e7be":
version "0.7.5"
- resolved "https://registry.yarnpkg.com/launchpad/-/launchpad-0.7.5.tgz#a16950c937572f10ef01c9be945a96f7aef8e427"
- integrity sha512-gsYFgT8XKL3X2XZHPPPrgwM0JqeQwGpSWnzg7EYadBY3MirbQrTVq6L4fm6l7UE2T+7gnfuhiGkKr/xxuU/fdw==
+ resolved "git+https://github.com/418sec/launchpad.git#de5aca11dc16a8e530195281c77614bdbb08e7be"
dependencies:
async "^2.0.1"
browserstack "^1.2.0"
@@ -8910,10 +8909,10 @@
dependencies:
minimalistic-assert "^1.0.0"
-wct-local@^2.1.1:
- version "2.1.5"
- resolved "https://registry.yarnpkg.com/wct-local/-/wct-local-2.1.5.tgz#f7986753e3ad9a35d39178a9989350523561fff1"
- integrity sha512-eqoZhjGy4Xq2tY0uB46Grkw/ztq+/rC0ImbYKl62unFHXtOgal+kkvnxR3SLRFNM8ty9+ItgycPeH0IpTqVL+w==
+wct-local@2.1.6, wct-local@^2.1.1:
+ version "2.1.6"
+ resolved "https://registry.yarnpkg.com/wct-local/-/wct-local-2.1.6.tgz#2d099c52996e77265d16e03a5d6d897b77ea9967"
+ integrity sha512-jvTzgOIIfJ43H3DXUfruHPTQ/TJ269SDk4R2CfCpU13EYbwxn3U1B6L5NHYRFu/cgdJmOHraGrn/wREHH6xeXQ==
dependencies:
"@types/express" "^4.0.30"
"@types/freeport" "^1.0.19"
@@ -8922,7 +8921,7 @@
chalk "^2.3.0"
cleankill "^2.0.0"
freeport "^1.0.4"
- launchpad "^0.7.0"
+ launchpad "git://github.com/418sec/launchpad.git#de5aca11dc16a8e530195281c77614bdbb08e7be"
selenium-standalone "^6.7.0"
which "^1.0.8"
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index 608c382b..71b736b 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -4,6 +4,8 @@
GUAVA_BIN_SHA1 = "00d0c3ce2311c9e36e73228da25a6e99b2ab826f"
+GUAVA_TESTLIB_BIN_SHA1 = "798c3827308605cd69697d8f1596a1735d3ef6e2"
+
GUAVA_DOC_URL = "https://google.github.io/guava/releases/" + GUAVA_VERSION + "/api/docs/"
def declare_nongoogle_deps():
@@ -23,8 +25,8 @@
# Transitive dependency of commons-compress
maven_jar(
name = "tukaani-xz",
- artifact = "org.tukaani:xz:1.8",
- sha1 = "c4f7d054303948eb6a4066194253886c8af07128",
+ artifact = "org.tukaani:xz:1.9",
+ sha1 = "1ea4bec1a921180164852c65006d928617bd2caf",
)
maven_jar(
@@ -33,18 +35,18 @@
sha1 = "cb2f351bf4463751201f43bb99865235d5ba07ca",
)
- SSHD_VERS = "2.6.0"
+ SSHD_VERS = "2.7.0"
maven_jar(
name = "sshd-osgi",
artifact = "org.apache.sshd:sshd-osgi:" + SSHD_VERS,
- sha1 = "40e365bb799e1bff3d31dc858b1e59a93c123f29",
+ sha1 = "a101aad0f79ad424498098f7e91c39d3d92177c1",
)
maven_jar(
name = "sshd-sftp",
artifact = "org.apache.sshd:sshd-sftp:" + SSHD_VERS,
- sha1 = "6eddfe8fdf59a3d9a49151e4177f8c1bebeb30c9",
+ sha1 = "0c9eff7145e20b338c1dd6aca36ba93ed7c0147c",
)
maven_jar(
@@ -62,7 +64,7 @@
maven_jar(
name = "sshd-mina",
artifact = "org.apache.sshd:sshd-mina:" + SSHD_VERS,
- sha1 = "d22138ba75dee95e2123f0e53a9c514b2a766da9",
+ sha1 = "22799941ec7bd5170ea890363cb968e400a69c41",
)
maven_jar(
@@ -99,24 +101,30 @@
# Google internal dependencies: these are developed at Google, so there is
# no concern about version skew.
- FLOGGER_VERS = "0.6"
+ maven_jar(
+ name = "error-prone-annotations",
+ artifact = "com.google.errorprone:error_prone_annotations:2.10.0",
+ sha1 = "9bc20b94d3ac42489cf6ce1e42509c86f6f861a1",
+ )
+
+ FLOGGER_VERS = "0.7.4"
maven_jar(
name = "flogger",
artifact = "com.google.flogger:flogger:" + FLOGGER_VERS,
- sha1 = "155dc6e303a58f7bbff5d2cd1a259de86827f4fe",
+ sha1 = "cec29ed8b58413c2e935d86b12d6b696dc285419",
)
maven_jar(
name = "flogger-log4j-backend",
artifact = "com.google.flogger:flogger-log4j-backend:" + FLOGGER_VERS,
- sha1 = "9743841bf10309163effd8ddf882b5d5190cc9d9",
+ sha1 = "7486b1c0138647cd7714eccb8ce37b5f2ae20a76",
)
maven_jar(
name = "flogger-system-backend",
artifact = "com.google.flogger:flogger-system-backend:" + FLOGGER_VERS,
- sha1 = "0f0ccf8923c6c315f2f57b108bcc6e46ccd88777",
+ sha1 = "4bee7ebbd97c63ca7fb17529aeb49a57b670d061",
)
maven_jar(
@@ -125,6 +133,12 @@
sha1 = GUAVA_BIN_SHA1,
)
+ maven_jar(
+ name = "guava-testlib",
+ artifact = "com.google.guava:guava-testlib:" + GUAVA_VERSION,
+ sha1 = GUAVA_TESTLIB_BIN_SHA1,
+ )
+
GUICE_VERS = "5.0.1"
maven_jar(
diff --git a/version.bzl b/version.bzl
index 10de529..4b6293b 100644
--- a/version.bzl
+++ b/version.bzl
@@ -2,4 +2,4 @@
# Used by :api_install and :api_deploy targets
# when talking to the destination repository.
#
-GERRIT_VERSION = "3.5.1-SNAPSHOT"
+GERRIT_VERSION = "3.6.0-SNAPSHOT"
diff --git a/yarn.lock b/yarn.lock
index 3ddfac6..c71ba52 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -577,7 +577,7 @@
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809"
integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
-chalk@^2.0.0, chalk@^2.4.2:
+chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -728,6 +728,17 @@
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
+cross-spawn@^6.0.5:
+ version "6.0.5"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+ integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+ dependencies:
+ nice-try "^1.0.4"
+ path-key "^2.0.1"
+ semver "^5.5.0"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -1027,6 +1038,32 @@
string.prototype.trimstart "^1.0.4"
unbox-primitive "^1.0.1"
+es-abstract@^1.19.1:
+ version "1.19.1"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3"
+ integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==
+ dependencies:
+ call-bind "^1.0.2"
+ es-to-primitive "^1.2.1"
+ function-bind "^1.1.1"
+ get-intrinsic "^1.1.1"
+ get-symbol-description "^1.0.0"
+ has "^1.0.3"
+ has-symbols "^1.0.2"
+ internal-slot "^1.0.3"
+ is-callable "^1.2.4"
+ is-negative-zero "^2.0.1"
+ is-regex "^1.1.4"
+ is-shared-array-buffer "^1.0.1"
+ is-string "^1.0.7"
+ is-weakref "^1.0.1"
+ object-inspect "^1.11.0"
+ object-keys "^1.1.1"
+ object.assign "^4.1.2"
+ string.prototype.trimend "^1.0.4"
+ string.prototype.trimstart "^1.0.4"
+ unbox-primitive "^1.0.1"
+
es-to-primitive@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
@@ -1520,6 +1557,14 @@
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
+get-symbol-description@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6"
+ integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==
+ dependencies:
+ call-bind "^1.0.2"
+ get-intrinsic "^1.1.1"
+
get-value@^2.0.3, get-value@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
@@ -1876,7 +1921,7 @@
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
-is-callable@^1.1.4, is-callable@^1.2.3:
+is-callable@^1.1.4, is-callable@^1.2.3, is-callable@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
@@ -2034,7 +2079,7 @@
dependencies:
isobject "^3.0.1"
-is-regex@^1.1.3:
+is-regex@^1.1.3, is-regex@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
@@ -2042,12 +2087,17 @@
call-bind "^1.0.2"
has-tostringtag "^1.0.0"
+is-shared-array-buffer@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6"
+ integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==
+
is-stream@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
-is-string@^1.0.5, is-string@^1.0.6:
+is-string@^1.0.5, is-string@^1.0.6, is-string@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==
@@ -2066,6 +2116,13 @@
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+is-weakref@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.1.tgz#842dba4ec17fa9ac9850df2d6efbc1737274f2a2"
+ integrity sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==
+ dependencies:
+ call-bind "^1.0.0"
+
is-windows@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
@@ -2331,6 +2388,11 @@
dependencies:
object-visit "^1.0.0"
+memorystream@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
+ integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI=
+
meow@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364"
@@ -2484,6 +2546,11 @@
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=
+nice-try@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+ integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+
normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
@@ -2509,6 +2576,21 @@
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a"
integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==
+npm-run-all@^4.1.5:
+ version "4.1.5"
+ resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba"
+ integrity sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==
+ dependencies:
+ ansi-styles "^3.2.1"
+ chalk "^2.4.1"
+ cross-spawn "^6.0.5"
+ memorystream "^0.3.1"
+ minimatch "^3.0.4"
+ pidtree "^0.3.0"
+ read-pkg "^3.0.0"
+ shell-quote "^1.6.1"
+ string.prototype.padend "^3.0.0"
+
npm-run-path@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
@@ -2733,6 +2815,11 @@
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+path-key@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+ integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
+
path-key@^3.0.0, path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
@@ -2760,6 +2847,11 @@
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
+pidtree@^0.3.0:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.1.tgz#ef09ac2cc0533df1f3250ccf2c4d366b0d12114a"
+ integrity sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==
+
pify@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
@@ -3095,7 +3187,7 @@
dependencies:
semver "^6.3.0"
-"semver@2 || 3 || 4 || 5":
+"semver@2 || 3 || 4 || 5", semver@^5.5.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@@ -3132,6 +3224,13 @@
is-plain-object "^2.0.3"
split-string "^3.0.1"
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
+ dependencies:
+ shebang-regex "^1.0.0"
+
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@@ -3139,11 +3238,21 @@
dependencies:
shebang-regex "^3.0.0"
+shebang-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+ integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+
shebang-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+shell-quote@^1.6.1:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123"
+ integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==
+
side-channel@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
@@ -3313,6 +3422,15 @@
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0"
+string.prototype.padend@^3.0.0:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.3.tgz#997a6de12c92c7cb34dc8a201a6c53d9bd88a5f1"
+ integrity sha512-jNIIeokznm8SD/TZISQsZKYu7RJyheFNt84DUPrh482GC8RVp2MKqm2O5oBRdGxbDQoXrhhWtPIWQOiy20svUg==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+ es-abstract "^1.19.1"
+
string.prototype.trimend@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
@@ -3734,6 +3852,13 @@
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
+which@^1.2.9:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+ integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+ dependencies:
+ isexe "^2.0.0"
+
which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"