Merge "Add dark theme colors for bulk vote dialog"
diff --git a/Documentation/cmd-create-project.txt b/Documentation/cmd-create-project.txt
index 0575eb9..358324d 100644
--- a/Documentation/cmd-create-project.txt
+++ b/Documentation/cmd-create-project.txt
@@ -136,13 +136,13 @@
project. Disabled by default.
--use-signed-off-by::
---so:
+--so::
If enabled, each change must contain a Signed-off-by line
from either the author or the uploader in the commit message.
Disabled by default.
--create-new-change-for-all-not-in-target::
---ncfa:
+--ncfa::
If enabled, a new change is created for every commit that is not in
the target branch. If the pushed commit is a merge commit, this flag is
ignored for that push. To avoid accidental creation of a large number
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index f0ad460..5fd0bfc 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -324,6 +324,19 @@
comment:: Review comment cover message.
+=== Project Head Updated
+
+Sent when project's head is updated.
+
+type:: "project-head-updated"
+
+oldHead:: The old project head name
+
+newHead:: The new project head name
+
+eventCreatedOn:: Time in seconds since the UNIX epoch when this event was
+created.
+
== SEE ALSO
* link:json.html[JSON Data Formats]
diff --git a/Documentation/concept-changes.txt b/Documentation/concept-changes.txt
index 0444fab..6e76a8a 100644
--- a/Documentation/concept-changes.txt
+++ b/Documentation/concept-changes.txt
@@ -34,6 +34,11 @@
|Owner
|The contributor who created the change.
+|Uploader
+|The user that uploaded the current patch set (e.g. the user that executed the
+`git push` command, or the user that triggered the patch set creation through
+an action in the UI).
+
|Assignee
|The contributor responsible for the change. Often used when a change has
mulitple reviewers to identify the individual responsible for final approval.
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index fcc8b7e..107473a 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -24,33 +24,31 @@
link:dev-roles.html#maintainer[maintainers,role=external,window=_blank]
review them adhoc.
-For large/complex features, it is required to follow the
-link:#design-driven-contribution-process[design-driven contribution
-process] and specify the feature in a link:dev-design-docs.html[design
-doc,role=external,window=_blank] before starting with the implementation.
+For large/complex features, it is required to specify the feature in a
+link:dev-design-docs.html[design document,role=external,window=_blank] before
+starting implementation, as per the
+link:#design-driven-contribution-process[design-driven contribution process].
If link:dev-roles.html#contributor[contributors,role=external,window=_blank]
-choose the lightweight contribution process and during the review it turns out
-that the feature is too large or complex,
-link:dev-roles.html#maintainer[maintainers,role=external,window=_blank] can
-require to follow the design-driven contribution process instead.
+choose the lightweight contribution process but the feature is found to be
+large or complex, link:dev-roles.html#maintainer[maintainers,role=external,window=_blank]
+can require that the design-driven contribution process be followed instead.
If you are in doubt which process is right for you, consult the
link:https://groups.google.com/d/forum/repo-discuss[repo-discuss,role=external,window=_blank]
mailing list.
These contribution processes apply to everyone who contributes code to
-the Gerrit project, including link:dev-roles.html#maintainer[
-maintainers,role=external,window=_blank]. When reading this document, keep in
-mind that maintainers are also contributors when they contribute code.
+the Gerrit project. link:dev-roles.html#maintainer[
+Maintainers,role=external,window=_blank] are also considered contributors
+when they contribute code.
If a new feature is large or complex, it is often difficult to find a
-maintainer who can take the time that is needed for a thorough review,
-and who can help with getting the changes submitted. To avoid that this
-results in unpredictable long waiting times during code review,
-contributors can ask for link:#mentorship[mentor support]. A mentor
-helps with timely code reviews and technical guidance. Doing the
-implementation is still the responsibility of the contributor.
+maintainer who can take the time that is needed for a thorough review. This
+can result in unpredictably long waiting times before the changes are
+submitted. To avoid that, contributors can ask for link:#mentorship[mentor support].
+A mentor helps with timely code reviews and technical guidance, though the
+implementation itself is still the responsibility of the contributor.
[[comparison]]
=== Quick Comparison
@@ -66,8 +64,8 @@
|Review |adhoc (when reviewer is available)|by a dedicated mentor (if
a link:#mentorship[mentor] was assigned)
|Caveats |features may get vetoed after the implementation was already
-done, maintainers may make the design-driven contribution process
-required if a change gets too complex/large|design doc must stay open
+done, maintainers may require the design-driven contribution process
+be followed if a change gets too complex/large|design doc must stay open
for a minimum of 10 calendar days, a mentor may not be available
immediately
|Applicable to|documentation updates, bug fixes, small features|
@@ -83,40 +81,32 @@
link:#design-driven-contribution-process[design-driven contribution
process] is required.
-As Gerrit is a code review tool, naturally contributions will
-be reviewed before they will get submitted to the code base. To
-start your contribution, please make a git commit and upload it
-for review to the link:https://gerrit-review.googlesource.com/[
-gerrit-review.googlesource.com,role=external,window=_blank] Gerrit server. To
-help speed up the review of your change, review these link:dev-crafting-changes.html[
+To start contributing to Gerrit, upload your git commit for review to the
+link:https://gerrit-review.googlesource.com/[gerrit-review.googlesource.com,
+role=external,window=_blank] Gerrit server. Review these link:dev-crafting-changes.html[
guidelines,role=external,window=_blank] before submitting your change. You can
-view the pending Gerrit contributions and their statuses
-link:https://gerrit-review.googlesource.com/#/q/status:open+project:gerrit[here,role=external,window=_blank].
+view pending contributions link:https://gerrit-review.googlesource.com/#/q/status:open+project:gerrit[here,role=external,window=_blank].
Depending on the size of that list it might take a while for
-your change to get reviewed. Naturally there are fewer
-link:dev-roles.html#maintainer[maintainers,role=external,window=_blank], that
-can approve changes, than link:dev-roles.html#contributor[contributors,role=external,window=_blank];
-so anything that you can do to ensure that your contribution will undergo fewer
-revisions will speed up the contribution process. This includes
-helping out reviewing other people's changes to relieve the load from
-the maintainers. Even if you are not familiar with Gerrit's internals,
+your change to get reviewed. Anything that you can do to ensure that your
+contribution will undergo fewer revisions will speed up the contribution process.
+This includes helping out reviewing other people's changes to relieve the
+load from the maintainers. Even if you are not familiar with Gerrit's internals,
it would be of great help if you can download, try out, and comment on
-new features. If it works as advertised, say so, and if you have the
+new features. If it works as advertised, say so, and if you have the
privileges to do so, go ahead and give it a `+1 Verified`. If you
would find the feature useful, say so and give it a `+1 Code Review`.
-And finally, the quicker you respond to the comments of your reviewers,
-the quicker your change might get merged! Try to reply to every
-comment after submitting your new patch, particularly if you decided
-against making the suggested change. Reviewers don't want to seem like
-nags and pester you if you haven't replied or made a fix, so it helps
-them know if you missed it or decided against it.
+Finally, the quicker you respond to the comments of your reviewers, the
+quicker your change can be merged! Try to reply to every comment after
+submitting your new patch, particularly if you decided against making the
+suggested change. Reviewers don't want to seem like nags and pester you
+if you haven't replied or made a fix, so it helps them know if you missed
+it or decided against it.
-Features or API extensions, even if they are small, will incur
-long-time maintenance and support burden, so they should be left
-pending for at least 24 hours to give maintainers in all timezones a
-chance to evaluate.
+A new feature or API extension, even if small, can incur a long-time
+maintenance and support burden and should be left pending for 24 hours
+to give maintainers in all time zones a chance to evaluate the change.
[[design-driven-contribution-process]]
=== Design-driven Contribution Process
@@ -126,19 +116,19 @@
For large/complex features it is important to:
-* agree on the functionality and scope before spending too much time
- on the implementation
+* agree on functionality and scope before spending too much time on
+ implementation
* ensure that they are in line with Gerrit's project scope and vision
* ensure that they are well aligned with other features
-* think about possibilities how the feature could be evolved over time
+* consider how the feature could be evolved over time
This is why for large/complex features it is required to describe the
feature in a link:dev-design-docs.html[design doc,role=external,window=_blank]
and get it approved by the
-link:dev-processes.html#steering-committee[steering committee,role=external,window=_blank],
+link:dev-processes.html#steering-committee[steering committee,role=external,window=_blank]
before starting the implementation.
-The design-driven contribution process has the following steps:
+The design-driven contribution process consists of the following steps:
* A link:dev-roles.html#contributor[contributor,role=external,window=_blank]
link:dev-design-docs.html#propose[proposes,role=external,window=_blank] a new
@@ -155,40 +145,31 @@
be accepted.
* To be submitted, the design doc needs to be approved by the
link:dev-processes.html#steering-committee[steering committee,role=external,window=_blank].
-* After the design was approved, the implementation is done by pushing
- changes for review, see link:#lightweight-contribution-process[
+* After the design is approved, it is implemented by pushing
+ changes for review, see the link:#lightweight-contribution-process[
lightweight contribution process]. Changes that are associated with
a design should all share a common hashtag. The contributor is the
- main driver of the implementation and responsible that it is done.
- Others from the Gerrit community are usually much welcome to help
- with the implementation.
+ main driver of the implementation and responsible for its completion.
+ Others from the Gerrit community are usually welcome to help.
-In order to be accepted/submitted, it is not necessary that the design
-doc fully specifies all the details, but the idea of the feature and
-how it fits into Gerrit should be sufficiently clear (judged by the
-steering committee). Contributors are expected to keep the design doc
-updated and fill in gaps while they go forward with the implementation.
-We expect that implementing the feature and updating the design doc
-will be an iterative process.
+The design doc does not need to fully specify each detail of the feature,
+but its concept and how it fits into Gerrit should be sufficiently clear,
+as judged by the steering committee. Contributors are expected to keep
+the design doc updated and fill in gaps while they go forward with the
+implementation. We expect that implementing the feature and updating the
+design doc will be an iterative process.
-While the design doc is still in review, contributors may already start
-with the implementation (e.g. do some prototyping to demonstrate parts
-of the proposed design), but those changes should not be submitted
-while the design wasn't approved yet. Another way to demonstrate the
-design can be to add screenshots or the like, early enough in the doc.
+While the design doc is still in review, contributors may start with the
+implementation (e.g. do some prototyping to demonstrate parts of the
+proposed design), but those changes should not be submitted while the
+design is not yet approved. Another way to demonstrate the design can be
+mocking screenshots in the doc.
By approving a design, the steering committee commits to:
* Accepting the feature when it is implemented.
* Supporting the feature by assigning a link:dev-roles.html#mentor[
- mentor,role=external,window=_blank] (if requested, see link:#mentorship[mentorship]).
-
-If the implementation of a feature gets stuck and it's unclear whether
-the feature gets fully done, it should be discussed with the steering
-committee how to proceed. If the contributor cannot commit to finish
-the implementation and no other contributor can take over, changes that
-have already been submitted for the feature might get reverted so that
-there is no unused or half-finished code in the code base.
+ mentor,role=external,window=_blank] if requested (see link:#mentorship[mentorship]).
For contributors, the design-driven contribution process has the
following advantages:
@@ -196,12 +177,11 @@
* By writing a design doc, the feature gets more attention. During the
design review, feedback from various sides can be collected, which
likely leads to improvements of the feature.
-* Once a design was approved by the
+* Once a design is approved by the
link:dev-processes.html#steering-committee[steering committee,role=external,window=_blank],
the contributor can be almost certain that the feature will be accepted.
- Hence, there is only a low risk to invest into implementing a feature
- and see it being rejected later during the code review, as it can
- happen with the lightweight contribution process.
+ Hence, there little risk of the feature being rejected later in code review,
+ as can occur with the lightweight contribution process.
* The contributor can link:#mentorship[get a dedicated mentor assigned]
who provides timely reviews and serves as a contact person for
technical questions and discussing details of the design.
@@ -249,12 +229,11 @@
* done criteria that define when the feature is done and the mentorship
ends
-If a feature is not finished in time, it should be discussed with the
-steering committee how to proceed. If the contributor cannot commit to
-finish the implementation in time and no other contributor can take
-over, changes that have already been submitted for the feature might
-get reverted so that there is no unused or half-finished code in the
-code base.
+If a feature implementation is not completed in time and no contributors
+can commit to finishing the implementation, changes that have already been
+submitted for the feature may be reverted to avoid unused or half-finished
+code in the code base. In these circumstances, the steering committee
+determines how to proceed.
[[esc-dd-evaluation]]
== How the ESC evaluates design documents
@@ -314,7 +293,7 @@
=== Core vs. Plugin decision
Q: `Would this fit better in a plugin?`
-* Yes:The proposed feature or rework is an implementation (e.g. Lucene
+* Yes: The proposed feature or rework is an implementation (e.g. Lucene
is an index implementation) of a generic concept that others
might want to implement differently.
* Yes: The proposed feature or rework is very specific to a custom setup.
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 9df4b04..70352dc 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -456,6 +456,15 @@
=== Group
* `group/guess_relevant_groups_latency`: Latency for guessing relevant groups.
+* `group/handles_count`: Number of calls to GroupBackend.handles.
+* `group/get_count`: Number of calls to GroupBackend.get.
+* `group/suggest_count`: Number of calls to GroupBackend.suggest.
+* `group/contains_count`: Number of calls to GroupMemberships.contains.
+* `group/contains_any_of_count`: Number of calls to
+ GroupMemberships.containsAnyOf.
+* `group/intersection_count`: Number of calls to GroupMemberships.intersection.
+* `group/known_groups_count`: Number of calls to GroupMemberships.getKnownGroups.
+
=== Replication Plugin
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index ae0c0a6..64845da 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -2658,7 +2658,7 @@
Allowed values are `10`, `25`, `50`, `100`.
|`theme` ||
Which theme to use.
-Allowed values are `DARK` or `LIGHT`.
+Allowed values are `AUTO` or `DARK` or `LIGHT`.
|`expand_inline_diffs` |not set if `false`|
Whether to expand diffs inline instead of opening as separate page
(Gerrit web app UI only).
@@ -2729,7 +2729,7 @@
Allowed values are `10`, `25`, `50`, `100`.
|`theme` |optional|
Which theme to use.
-Allowed values are `DARK` or `LIGHT`.
+Allowed values are `AUTO` or `DARK` or `LIGHT`.
|`expand_inline_diffs` |not set if `false`|
Whether to expand diffs inline instead of opening as separate page
(Gerrit web app UI only).
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 6fa584ac..a61d86d 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -3393,6 +3393,133 @@
HTTP/1.1 200 OK
----
+[[submit-requirement-endpoints]]
+== Submit Requirement Endpoints
+
+[[create-submit-requirement]]
+=== Create Submit Requirement
+
+--
+'PUT /projects/link:#project-name[\{project-name\}]/submit_requirements/link:#submit-requirement-name[\{submit-requirement-name\}]'
+--
+
+Creates a new submit requirement definition in this project.
+
+The calling user must have write access to the `refs/meta/config` branch of the
+project.
+
+If a submit requirement with this name is already defined in this project, this
+submit requirement definition is updated
+(see link:#update-submit-requirement[Update Submit Requirement]).
+
+The submit requirement data must be provided in the request body as
+link:#submit-requirement-input[SubmitRequirementInput].
+
+.Request
+----
+ PUT /projects/My-Project/submit_requirements/Code-Review HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "name": "Code-Review",
+ "description": "At least one maximum vote for the Code-Review label is required",
+ "submittability_expression": "label:Code-Review=+2"
+ }
+----
+
+As response a link:#submit-requirement-info[SubmitRequirementInfo] entity is
+returned that describes the created submit requirement.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "name": "Code-Review",
+ "description": "At least one maximum vote for the Code-Review label is required",
+ "submittability_expression": "label:Code-Review=+2",
+ "allow_override_in_child_projects": false
+ }
+----
+
+[[update-submit-requirement]]
+=== Update Submit Requirement
+
+--
+'PUT /projects/link:#project-name[\{project-name\}]/submit_requirements/link:#submit-requirement-name[\{submit-requirement-name\}]'
+--
+
+Updates the definition of a submit requirement that is defined in this project.
+
+The calling user must have write access to the `refs/meta/config` branch of the
+project.
+
+The new submit requirement will overwrite the existing submit requirement.
+That is, unspecified attributes will be set to their defaults.
+
+.Request
+----
+ PUT /projects/My-Project/submit_requirements/Code-Review HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "name": "Code-Review",
+ "description": "At least one maximum vote for the Code-Review label is required",
+ "submittability_expression": "label:Code-Review=+2"
+ }
+----
+
+As response a link:#submit-requirement-info[SubmitRequirementInfo] entity is
+returned that describes the created submit requirement.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "name": "Code-Review",
+ "description": "At least one maximum vote for the Code-Review label is required",
+ "submittability_expression": "label:Code-Review=+2",
+ "allow_override_in_child_projects": false
+ }
+----
+
+[[get-submit-requirement]]
+=== Get Submit Requirement
+--
+'GET /projects/link:#project-name[\{project-name\}]/submit_requirements/link:#submit-requirement-name[\{submit-requirement-name\}]'
+--
+
+Retrieves the definition of a submit requirement that is defined in this project.
+
+The calling user must have read access to the `refs/meta/config` branch of the
+project.
+
+.Request
+----
+ GET /projects/All-Projects/submit-requirement/Code-Review HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "name": "Code-Review",
+ "description": "At least one maximum vote for the Code-Review label is required",
+ "submittability_expression": "label:Code-Review=+2",
+ "allow_override_in_child_projects": false
+ }
+----
[[ids]]
== IDs
@@ -3421,6 +3548,11 @@
=== \{label-name\}
The name of a review label.
+[[submit-requirement-name]]
+=== \{submit-requirement-name\}
+The name of a submit requirement.
+
+
[[project-name]]
=== \{project-name\}
The name of the project.
@@ -3970,7 +4102,7 @@
|`value` ||
The effective boolean value.
|`configured_value` ||
-The configured value, can be `TRUE`, `FALSE` or `INHERITED`.
+The configured value, can be `TRUE`, `FALSE` or `INHERIT`.
|`inherited_value` |optional|
The boolean value inherited from the parent. +
Not set if there is no parent.
@@ -4366,6 +4498,57 @@
|`size_of_packed_objects` |Size of packed objects in bytes.
|======================================
+[[submit-requirement-info]]
+=== SubmitRequirementInfo
+The `SubmitRequirementInfo` entity describes a submit requirement.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name ||Description
+|`name`||
+The submit requirement name.
+|`description`|optional|
+Description of the submit requirement.
+|`applicability_expression`|optional|
+Query expression that can be evaluated on any change. If evaluated to true on a
+change, the submit requirement is then applicable for this change.
+If not specified, the submit requirement is applicable for all changes.
+|`submittability_expression`||
+Query expression that can be evaluated on any change. If evaluated to true on a
+change, the submit requirement is fulfilled and not blocking change submission.
+|`override_expression`|optional|
+Query expression that can be evaluated on any change. If evaluated to true on a
+change, the submit requirement is overridden and not blocking change submission.
+|`allow_override_in_child_projects`||
+Whether this submit requirement can be overridden in child projects.
+|===========================
+
+[[submit-requirement-input]]
+=== SubmitRequirementInput
+The `SubmitRequirementInput` entity describes a submit requirement.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name ||Description
+|`name`||
+The submit requirement name.
+|`description`|optional|
+Description of the submit requirement.
+|`applicability_expression`|optional|
+Query expression that can be evaluated on any change. If evaluated to true on a
+change, the submit requirement is then applicable for this change.
+If not specified, the submit requirement is applicable for all changes.
+|`submittability_expression`||
+Query expression that can be evaluated on any change. If evaluated to true on a
+change, the submit requirement is fulfilled and not blocking change submission.
+|`override_expression`|optional|
+Query expression that can be evaluated on any change. If evaluated to true on a
+change, the submit requirement is overridden and not blocking change submission.
+|`allow_override_in_child_projects`|optional|
+Whether this submit requirement can be overridden in child projects. Default is
+`false`.
+|===========================
+
[[submit-type-info]]
=== SubmitTypeInfo
Information about the link:config-project-config.html#submit-type[default submit
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index f716cb0..7816f5f 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -363,7 +363,7 @@
files, use `file:^.*\.java`.
+
The entire regular expression pattern, including the `^` character,
-should be double quoted. For example, to match all XML
+can be double quoted. For example, to match all XML
files named like 'name1.xml', 'name2.xml', and 'name3.xml' use
`file:"^name[1-3].xml"`.
+
@@ -371,8 +371,8 @@
+
*More examples:*
-* `-file:^path/.*` - changes that do not modify files from `path/`.
-* `file:{^~(path/.*)}` - changes that modify files not from `path/` (but may
+* `-path:^path/.*` - changes that do not modify files from `path/`.
+* `path:{^~(path/.*)}` - changes that modify files not from `path/` (but may
contain files from `path/`).
[[file]]
diff --git a/java/com/google/gerrit/acceptance/ExtensionRegistry.java b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
index 0acf3bc..3d90bf0 100644
--- a/java/com/google/gerrit/acceptance/ExtensionRegistry.java
+++ b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
@@ -21,6 +21,7 @@
import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
import com.google.gerrit.extensions.events.AccountActivationListener;
import com.google.gerrit.extensions.events.AccountIndexedListener;
+import com.google.gerrit.extensions.events.AttentionSetListener;
import com.google.gerrit.extensions.events.ChangeIndexedListener;
import com.google.gerrit.extensions.events.CommentAddedListener;
import com.google.gerrit.extensions.events.GitBatchRefUpdateListener;
@@ -103,6 +104,7 @@
private final DynamicSet<OnPostReview> onPostReviews;
private final DynamicSet<ReviewerAddedListener> reviewerAddedListeners;
private final DynamicSet<ReviewerDeletedListener> reviewerDeletedListeners;
+ private final DynamicSet<AttentionSetListener> attentionSetListeners;
private final DynamicMap<ChangeHasOperandFactory> hasOperands;
private final DynamicMap<ChangeIsOperandFactory> isOperands;
@@ -147,7 +149,8 @@
DynamicSet<ReviewerAddedListener> reviewerAddedListeners,
DynamicSet<ReviewerDeletedListener> reviewerDeletedListeners,
DynamicMap<ChangeHasOperandFactory> hasOperands,
- DynamicMap<ChangeIsOperandFactory> isOperands) {
+ DynamicMap<ChangeIsOperandFactory> isOperands,
+ DynamicSet<AttentionSetListener> attentionSetListeners) {
this.accountIndexedListeners = accountIndexedListeners;
this.changeIndexedListeners = changeIndexedListeners;
this.groupIndexedListeners = groupIndexedListeners;
@@ -187,6 +190,7 @@
this.reviewerDeletedListeners = reviewerDeletedListeners;
this.hasOperands = hasOperands;
this.isOperands = isOperands;
+ this.attentionSetListeners = attentionSetListeners;
}
public Registration newRegistration() {
@@ -330,6 +334,10 @@
return add(workInProgressStateChangedListeners, workInProgressStateChangedListener);
}
+ public Registration add(AttentionSetListener attentionSetListener) {
+ return add(attentionSetListeners, attentionSetListener);
+ }
+
public Registration add(CapabilityDefinition capabilityDefinition, String exportName) {
return add(capabilityDefinitions, capabilityDefinition, exportName);
}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
index b91a56a..c1029be 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
@@ -47,7 +47,6 @@
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.Inject;
import java.io.IOException;
-import java.sql.Timestamp;
import java.time.Instant;
import java.util.Arrays;
import java.util.Objects;
@@ -140,8 +139,7 @@
RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
Instant now = TimeUtil.now();
IdentifiedUser changeOwner = getChangeOwner(changeCreation);
- PersonIdent authorAndCommitter =
- changeOwner.newCommitterIdent(now, serverIdent.getTimeZone());
+ PersonIdent authorAndCommitter = changeOwner.newCommitterIdent(now, serverIdent.getZoneId());
ObjectId commitId =
createCommit(repository, revWalk, objectInserter, changeCreation, authorAndCommitter);
@@ -496,13 +494,10 @@
return Optional.ofNullable(oldPatchsetCommit.getAuthorIdent()).orElse(serverIdent);
}
- // 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().toInstant())) {
+ if (asSeconds(now) == asSeconds(oldPatchsetCommitter.getWhenAsInstant())) {
/* 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,
@@ -512,7 +507,7 @@
* here and simply add a second. */
now = now.plusSeconds(1);
}
- return new PersonIdent(oldPatchsetCommitter, Timestamp.from(now));
+ return new PersonIdent(oldPatchsetCommitter, now);
}
private long asSeconds(Instant date) {
diff --git a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index 59475a4..587c2c7 100644
--- a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -227,6 +227,8 @@
LabelApi label(String labelName) throws RestApiException;
+ SubmitRequirementApi submitRequirement(String name) throws RestApiException;
+
/**
* Adds, updates and deletes label definitions in a batch.
*
@@ -426,6 +428,11 @@
}
@Override
+ public SubmitRequirementApi submitRequirement(String name) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public void labels(BatchLabelInput input) throws RestApiException {
throw new NotImplementedException();
}
diff --git a/java/com/google/gerrit/extensions/api/projects/SubmitRequirementApi.java b/java/com/google/gerrit/extensions/api/projects/SubmitRequirementApi.java
new file mode 100644
index 0000000..a6e79db
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/projects/SubmitRequirementApi.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.projects;
+
+import com.google.gerrit.extensions.common.SubmitRequirementInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementInput;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+public interface SubmitRequirementApi {
+ /** Create a new submit requirement. */
+ SubmitRequirementApi create(SubmitRequirementInput input) throws RestApiException;
+
+ /** Get existing submit requirement. */
+ SubmitRequirementInfo get() throws RestApiException;
+
+ /** Update existing submit requirement. */
+ SubmitRequirementInfo update(SubmitRequirementInput input) throws RestApiException;
+
+ /** Delete existing submit requirement. */
+ void delete() throws RestApiException;
+
+ /**
+ * A default implementation which allows source compatibility when adding new methods to the
+ * interface.
+ */
+ class NotImplemented implements SubmitRequirementApi {
+ @Override
+ public SubmitRequirementApi create(SubmitRequirementInput input) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public SubmitRequirementInfo get() throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public SubmitRequirementInfo update(SubmitRequirementInput input) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public void delete() throws RestApiException {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
index 6acf3f4..7ece6f6 100644
--- a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
+++ b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
@@ -102,6 +102,7 @@
}
public enum Theme {
+ AUTO,
DARK,
LIGHT
}
diff --git a/java/com/google/gerrit/extensions/common/SubmitRequirementInfo.java b/java/com/google/gerrit/extensions/common/SubmitRequirementInfo.java
new file mode 100644
index 0000000..9347e7e
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/SubmitRequirementInfo.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+public class SubmitRequirementInfo {
+ /** Name of the submit requirement. */
+ public String name;
+
+ /** Description of the submit requirement. */
+ public String description;
+
+ /**
+ * Expression string to be evaluated on a change. Decides if this submit requirement is applicable
+ * on the given change.
+ */
+ public String applicabilityExpression;
+
+ /**
+ * Expression string to be evaluated on a change. When evaluated to true, this submit requirement
+ * becomes fulfilled for this change.
+ */
+ public String submittabilityExpression;
+
+ /**
+ * Expression string to be evaluated on a change. When evaluated to true, this submit requirement
+ * becomes fulfilled for this change regardless of the evaluation of the {@link
+ * #submittabilityExpression}.
+ */
+ public String overrideExpression;
+
+ /** Boolean indicating if this submit requirement can be overridden in child projects. */
+ public boolean allowOverrideInChildProjects;
+}
diff --git a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
index cb9d855..f75ec66 100644
--- a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
@@ -72,14 +72,14 @@
// 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(gitPerson.date.getTime()).isEqualTo(ident.getWhen().getTime());
+ check("roundedDate()")
+ .that(gitPerson.date.getTime())
+ .isEqualTo(ident.getWhenAsInstant().toEpochMilli());
tz().isEqualTo(ident.getTimeZoneOffset());
}
}
diff --git a/java/com/google/gerrit/extensions/events/AttentionSetListener.java b/java/com/google/gerrit/extensions/events/AttentionSetListener.java
new file mode 100644
index 0000000..ada30ce
--- /dev/null
+++ b/java/com/google/gerrit/extensions/events/AttentionSetListener.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import java.util.Set;
+
+/** Notified whenever the attention set is changed. */
+@ExtensionPoint
+public interface AttentionSetListener {
+ interface Event extends ChangeEvent {
+
+ /**
+ * Returns the users added to the attention set because of this change
+ *
+ * @return Account IDs
+ */
+ Set<Integer> usersAdded();
+
+ /**
+ * Returns the users removed from the attention set because of this change
+ *
+ * @return Account IDs
+ */
+ Set<Integer> usersRemoved();
+ }
+
+ /**
+ * This function will be called when the attention set changes
+ *
+ * @param event The event that changed the attention set
+ */
+ void onAttentionSetChanged(Event event);
+}
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index f38653d..543e794 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -361,7 +361,7 @@
try (TraceContext traceContext = enableTracing(req, res)) {
String requestUri = requestUri(req);
- try (PerThreadCache ignored = PerThreadCache.create(req)) {
+ try (PerThreadCache ignored = PerThreadCache.create()) {
List<IdString> path = splitPath(req);
RequestInfo requestInfo = createRequestInfo(traceContext, requestUri(req), path);
globals.requestListeners.runEach(l -> l.onRequest(requestInfo));
diff --git a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
index 3cc5f9b..52955d3 100644
--- a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
+++ b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
@@ -38,7 +38,10 @@
@Deprecated static final Schema<ProjectData> V3 = schema(V2);
// Lucene index was changed to add an additional field for sorting.
- static final Schema<ProjectData> V4 = schema(V3);
+ @Deprecated static final Schema<ProjectData> V4 = schema(V3);
+
+ // Upgrade Lucene to 7.x requires reindexing.
+ static final Schema<ProjectData> V5 = schema(V4);
/**
* Name of the project index to be used when contacting index backends or loading configurations.
diff --git a/java/com/google/gerrit/json/OptionalTypeAdapter.java b/java/com/google/gerrit/json/OptionalTypeAdapter.java
new file mode 100644
index 0000000..9bfa72d
--- /dev/null
+++ b/java/com/google/gerrit/json/OptionalTypeAdapter.java
@@ -0,0 +1,69 @@
+// 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.json;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.inject.TypeLiteral;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Optional;
+
+public class OptionalTypeAdapter
+ implements JsonSerializer<Optional<?>>, JsonDeserializer<Optional<?>> {
+
+ private static final String VALUE = "value";
+
+ @Override
+ public JsonElement serialize(Optional<?> src, Type typeOfSrc, JsonSerializationContext context) {
+ Optional<?> optional = src == null ? Optional.empty() : src;
+ JsonObject json = new JsonObject();
+ json.add(VALUE, optional.map(context::serialize).orElse(JsonNull.INSTANCE));
+ return json;
+ }
+
+ @Override
+ public Optional<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+ throws JsonParseException {
+ if (!json.getAsJsonObject().has(VALUE)) {
+ return Optional.empty();
+ }
+
+ JsonElement value = json.getAsJsonObject().get(VALUE);
+ if (value == null || value.isJsonNull()) {
+ return Optional.empty();
+ }
+
+ // handle the situation when one uses Optional without type parameter which is an equivalent of
+ // <?> type
+ ParameterizedType parameterizedType =
+ (ParameterizedType) new TypeLiteral<Optional<?>>() {}.getType();
+ if (typeOfT instanceof ParameterizedType) {
+ parameterizedType = (ParameterizedType) typeOfT;
+ if (parameterizedType.getActualTypeArguments().length != 1) {
+ throw new JsonParseException("Expected one parameter type in Optional.");
+ }
+ }
+
+ Type optionalOf = parameterizedType.getActualTypeArguments()[0];
+ return Optional.of(context.deserialize(value, optionalOf));
+ }
+}
diff --git a/java/com/google/gerrit/lucene/LuceneAccountIndex.java b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
index 934b27f..fe3fc15 100644
--- a/java/com/google/gerrit/lucene/LuceneAccountIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
@@ -72,7 +72,7 @@
private static Term idTerm(boolean useLegacyNumericFields, Account.Id id) {
FieldDef<AccountState, ?> idField = useLegacyNumericFields ? ID : ID_STR;
if (useLegacyNumericFields) {
- return QueryBuilder.intTerm(idField.getName(), id.get());
+ return QueryBuilder.intTerm(idField.getName());
}
return QueryBuilder.stringTerm(idField.getName(), Integer.toString(id.get()));
}
diff --git a/java/com/google/gerrit/lucene/QueryBuilder.java b/java/com/google/gerrit/lucene/QueryBuilder.java
index bd34743..14ad528 100644
--- a/java/com/google/gerrit/lucene/QueryBuilder.java
+++ b/java/com/google/gerrit/lucene/QueryBuilder.java
@@ -49,7 +49,7 @@
public class QueryBuilder<V> {
/** @param name field name qparam i key value */
- static Term intTerm(String name, int i) {
+ static Term intTerm(String name) {
checkState(false, "Lucene index implementation removed legacy numeric type");
return null;
}
diff --git a/java/com/google/gerrit/pgm/init/AccountsOnInit.java b/java/com/google/gerrit/pgm/init/AccountsOnInit.java
index c56a8d9..be5fe1a 100644
--- a/java/com/google/gerrit/pgm/init/AccountsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/AccountsOnInit.java
@@ -30,7 +30,6 @@
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;
@@ -61,16 +60,12 @@
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(), Date.from(account.registeredOn()));
+ new PersonIdent(new GerritPersonIdentProvider(flags.cfg).get(), account.registeredOn());
Config accountConfig = new Config();
AccountProperties.writeToAccountConfig(
diff --git a/java/com/google/gerrit/pgm/init/GroupsOnInit.java b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
index 020705e..f8fcadd 100644
--- a/java/com/google/gerrit/pgm/init/GroupsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
@@ -40,7 +40,6 @@
import java.io.File;
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;
@@ -166,14 +165,10 @@
return AuditLogFormatter.createBackedBy(ImmutableSet.of(account), ImmutableSet.of(), serverId);
}
- // 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(), Timestamp.from(groupCreatedOn));
+ new PersonIdent(new GerritPersonIdentProvider(flags.cfg).get(), groupCreatedOn);
try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate(repository, personIdent)) {
groupConfig.commit(metaDataUpdate);
}
diff --git a/java/com/google/gerrit/server/CommonConverters.java b/java/com/google/gerrit/server/CommonConverters.java
index e7fd1c5..4ad143b 100644
--- a/java/com/google/gerrit/server/CommonConverters.java
+++ b/java/com/google/gerrit/server/CommonConverters.java
@@ -25,14 +25,11 @@
* 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.setDate(ident.getWhen().toInstant());
+ result.setDate(ident.getWhenAsInstant());
result.tz = ident.getTimeZoneOffset();
return result;
}
diff --git a/java/com/google/gerrit/server/IdentifiedUser.java b/java/com/google/gerrit/server/IdentifiedUser.java
index 122e18d..65a81f7 100644
--- a/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/java/com/google/gerrit/server/IdentifiedUser.java
@@ -50,9 +50,9 @@
import java.net.SocketAddress;
import java.net.URL;
import java.time.Instant;
+import java.time.ZoneId;
import java.util.Optional;
import java.util.Set;
-import java.util.TimeZone;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.util.SystemReader;
@@ -427,10 +427,10 @@
}
public PersonIdent newRefLogIdent() {
- return newRefLogIdent(Instant.now(), TimeZone.getDefault());
+ return newRefLogIdent(Instant.now(), ZoneId.systemDefault());
}
- public PersonIdent newRefLogIdent(Instant when, TimeZone tz) {
+ public PersonIdent newRefLogIdent(Instant when, ZoneId zoneId) {
final Account ua = getAccount();
String name = ua.fullName();
@@ -451,21 +451,18 @@
: ua.preferredEmail();
}
- return newPersonIdent(name, user, when, tz);
+ return new PersonIdent(name, user, when, zoneId);
}
private String constructMailAddress(Account ua, String host) {
return getUserName().orElse("") + "|account-" + ua.id().toString() + "@" + host;
}
- // 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());
+ return newCommitterIdent(ident.getWhenAsInstant(), ident.getZoneId());
}
- public PersonIdent newCommitterIdent(Instant when, TimeZone tz) {
+ public PersonIdent newCommitterIdent(Instant when, ZoneId zoneId) {
final Account ua = getAccount();
String name = ua.fullName();
String email = ua.preferredEmail();
@@ -500,7 +497,7 @@
}
}
- return newPersonIdent(name, email, when, tz);
+ return new PersonIdent(name, email, when, zoneId);
}
@Override
@@ -568,19 +565,4 @@
}
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/account/AccountConfig.java b/java/com/google/gerrit/server/account/AccountConfig.java
index 28e881e1..4143f77 100644
--- a/java/com/google/gerrit/server/account/AccountConfig.java
+++ b/java/com/google/gerrit/server/account/AccountConfig.java
@@ -36,7 +36,6 @@
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
-import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -258,9 +257,6 @@
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();
@@ -279,8 +275,8 @@
}
Instant registeredOn = loadedAccountProperties.get().getRegisteredOn();
- commit.setAuthor(new PersonIdent(commit.getAuthor(), Date.from(registeredOn)));
- commit.setCommitter(new PersonIdent(commit.getCommitter(), Date.from(registeredOn)));
+ commit.setAuthor(new PersonIdent(commit.getAuthor(), registeredOn));
+ commit.setCommitter(new PersonIdent(commit.getCommitter(), registeredOn));
}
saveAccount();
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index 3ee6365..137fd59 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -319,17 +319,13 @@
* @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(
ImmutableList.of(
repo -> {
AccountConfig accountConfig = read(repo, accountId);
- Account account =
- accountConfig.getNewAccount(committerIdent.getWhen().toInstant());
+ Account account = accountConfig.getNewAccount(committerIdent.getWhenAsInstant());
AccountState accountState = AccountState.forAccount(account);
AccountDelta.Builder deltaBuilder = AccountDelta.builder();
init.configure(accountState, deltaBuilder);
diff --git a/java/com/google/gerrit/server/account/Emails.java b/java/com/google/gerrit/server/account/Emails.java
index 45f0844..8c3f033 100644
--- a/java/com/google/gerrit/server/account/Emails.java
+++ b/java/com/google/gerrit/server/account/Emails.java
@@ -113,14 +113,11 @@
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(who.getWhen().toInstant());
+ u.setDate(who.getWhenAsInstant());
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/UniversalGroupBackend.java b/java/com/google/gerrit/server/account/UniversalGroupBackend.java
index 5bd9bea..1587bc5 100644
--- a/java/com/google/gerrit/server/account/UniversalGroupBackend.java
+++ b/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -27,10 +27,16 @@
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.GroupDescription;
import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.metrics.Counter1;
+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.CurrentUser;
import com.google.gerrit.server.StartupCheck;
import com.google.gerrit.server.StartupException;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.plugincontext.PluginSetEntryContext;
import com.google.gerrit.server.project.ProjectState;
@@ -49,11 +55,57 @@
public class UniversalGroupBackend implements GroupBackend {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final Field<String> SYSTEM_FIELD =
+ Field.ofString("system", Metadata.Builder::groupSystem).build();
+
private final PluginSetContext<GroupBackend> backends;
+ private final Counter1<String> handlesCount;
+ private final Counter1<String> getCount;
+ private final Counter2<String, Integer> suggestCount;
+ private final Counter2<String, Boolean> containsCount;
+ private final Counter2<String, Boolean> containsAnyCount;
+ private final Counter2<String, Integer> intersectionCount;
+ private final Counter2<String, Integer> knownGroupsCount;
@Inject
- UniversalGroupBackend(PluginSetContext<GroupBackend> backends) {
+ UniversalGroupBackend(PluginSetContext<GroupBackend> backends, MetricMaker metricMaker) {
this.backends = backends;
+ this.handlesCount =
+ metricMaker.newCounter(
+ "group/handles_count", new Description("Calls to GroupBackend.handles"), SYSTEM_FIELD);
+ this.getCount =
+ metricMaker.newCounter(
+ "group/get_count", new Description("Calls to GroupBackend.get"), SYSTEM_FIELD);
+ this.suggestCount =
+ metricMaker.newCounter(
+ "group/suggest_count",
+ new Description("Calls to GroupBackend.suggest"),
+ SYSTEM_FIELD,
+ Field.ofInteger("num_suggested", (meta, value) -> {}).build());
+ this.containsCount =
+ metricMaker.newCounter(
+ "group/contains_count",
+ new Description("Calls to GroupMemberships.contains"),
+ SYSTEM_FIELD,
+ Field.ofBoolean("contains", (meta, value) -> {}).build());
+ this.containsAnyCount =
+ metricMaker.newCounter(
+ "group/contains_any_of_count",
+ new Description("Calls to GroupMemberships.containsAnyOf"),
+ SYSTEM_FIELD,
+ Field.ofBoolean("contains_any_of", (meta, value) -> {}).build());
+ this.intersectionCount =
+ metricMaker.newCounter(
+ "group/intersection_count",
+ new Description("Calls to GroupMemberships.intersection"),
+ SYSTEM_FIELD,
+ Field.ofInteger("num_intersection", (meta, value) -> {}).build());
+ this.knownGroupsCount =
+ metricMaker.newCounter(
+ "group/known_groups_count",
+ new Description("Calls to GroupMemberships.getKnownGroups"),
+ SYSTEM_FIELD,
+ Field.ofInteger("num_known_groups", (meta, value) -> {}).build());
}
@Nullable
@@ -70,7 +122,12 @@
@Override
public boolean handles(AccountGroup.UUID uuid) {
- return backend(uuid) != null;
+ GroupBackend b = backend(uuid);
+ if (b == null) {
+ return false;
+ }
+ handlesCount.increment(name(b));
+ return true;
}
@Override
@@ -83,13 +140,19 @@
logger.atFine().log("Unknown GroupBackend for UUID: %s", uuid);
return null;
}
+ getCount.increment(name(b));
return b.get(uuid);
}
@Override
public Collection<GroupReference> suggest(String name, ProjectState project) {
Set<GroupReference> groups = Sets.newTreeSet(GROUP_REF_NAME_COMPARATOR);
- backends.runEach(g -> groups.addAll(g.suggest(name, project)));
+ backends.runEach(
+ g -> {
+ Collection<GroupReference> suggestions = g.suggest(name, project);
+ suggestCount.increment(name(g), suggestions.size());
+ groups.addAll(suggestions);
+ });
return groups;
}
@@ -108,11 +171,11 @@
}
@Nullable
- private GroupMembership membership(AccountGroup.UUID uuid) {
+ private Map.Entry<GroupBackend, GroupMembership> membership(AccountGroup.UUID uuid) {
if (uuid != null) {
for (Map.Entry<GroupBackend, GroupMembership> m : memberships.entrySet()) {
if (m.getKey().handles(uuid)) {
- return m.getValue();
+ return m;
}
}
}
@@ -125,51 +188,57 @@
if (uuid == null) {
return false;
}
- GroupMembership m = membership(uuid);
+ Map.Entry<GroupBackend, GroupMembership> m = membership(uuid);
if (m == null) {
return false;
}
- return m.contains(uuid);
+ boolean contains = m.getValue().contains(uuid);
+ containsCount.increment(name(m.getKey()), contains);
+ return contains;
}
@Override
public boolean containsAnyOf(Iterable<AccountGroup.UUID> uuids) {
- ListMultimap<GroupMembership, AccountGroup.UUID> lookups =
+ ListMultimap<Map.Entry<GroupBackend, GroupMembership>, AccountGroup.UUID> lookups =
MultimapBuilder.hashKeys().arrayListValues().build();
for (AccountGroup.UUID uuid : uuids) {
if (uuid == null) {
continue;
}
- GroupMembership m = membership(uuid);
+ Map.Entry<GroupBackend, GroupMembership> m = membership(uuid);
if (m == null) {
continue;
}
lookups.put(m, uuid);
}
- for (Map.Entry<GroupMembership, Collection<AccountGroup.UUID>> entry :
- lookups.asMap().entrySet()) {
- GroupMembership m = entry.getKey();
- Collection<AccountGroup.UUID> ids = entry.getValue();
+ for (Map.Entry<GroupBackend, GroupMembership> groupBackends : lookups.asMap().keySet()) {
+
+ GroupMembership m = groupBackends.getValue();
+ Collection<AccountGroup.UUID> ids = lookups.asMap().get(groupBackends);
if (ids.size() == 1) {
if (m.contains(Iterables.getOnlyElement(ids))) {
+ containsAnyCount.increment(name(groupBackends.getKey()), true);
return true;
}
} else if (m.containsAnyOf(ids)) {
+ containsAnyCount.increment(name(groupBackends.getKey()), true);
return true;
}
+ // We would have returned if contains was true.
+ containsAnyCount.increment(name(groupBackends.getKey()), false);
}
return false;
}
@Override
public Set<AccountGroup.UUID> intersection(Iterable<AccountGroup.UUID> uuids) {
- ListMultimap<GroupMembership, AccountGroup.UUID> lookups =
+ ListMultimap<Map.Entry<GroupBackend, GroupMembership>, AccountGroup.UUID> lookups =
MultimapBuilder.hashKeys().arrayListValues().build();
for (AccountGroup.UUID uuid : uuids) {
if (uuid == null) {
continue;
}
- GroupMembership m = membership(uuid);
+ Map.Entry<GroupBackend, GroupMembership> m = membership(uuid);
if (m == null) {
logger.atFine().log("Unknown GroupMembership for UUID: %s", uuid);
continue;
@@ -177,9 +246,11 @@
lookups.put(m, uuid);
}
Set<AccountGroup.UUID> groups = new HashSet<>();
- for (Map.Entry<GroupMembership, Collection<AccountGroup.UUID>> entry :
- lookups.asMap().entrySet()) {
- groups.addAll(entry.getKey().intersection(entry.getValue()));
+ for (Map.Entry<GroupBackend, GroupMembership> groupBackend : lookups.asMap().keySet()) {
+ Set<AccountGroup.UUID> intersection =
+ groupBackend.getValue().intersection(lookups.asMap().get(groupBackend));
+ intersectionCount.increment(name(groupBackend.getKey()), intersection.size());
+ groups.addAll(intersection);
}
return groups;
}
@@ -187,8 +258,10 @@
@Override
public Set<AccountGroup.UUID> getKnownGroups() {
Set<AccountGroup.UUID> groups = new HashSet<>();
- for (GroupMembership m : memberships.values()) {
- groups.addAll(m.getKnownGroups());
+ for (Map.Entry<GroupBackend, GroupMembership> entry : memberships.entrySet()) {
+ Set<AccountGroup.UUID> knownGroups = entry.getValue().getKnownGroups();
+ knownGroupsCount.increment(name(entry.getKey()), knownGroups.size());
+ groups.addAll(knownGroups);
}
return groups;
}
@@ -204,6 +277,13 @@
return false;
}
+ private static String name(GroupBackend backend) {
+ if (backend == null) {
+ return "none";
+ }
+ return backend.getClass().getSimpleName();
+ }
+
public static class ConfigCheck implements StartupCheck {
private final Config cfg;
private final UniversalGroupBackend universalGroupBackend;
diff --git a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index 5baed86..f1620cc 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -41,6 +41,7 @@
import com.google.gerrit.extensions.api.projects.ParentInput;
import com.google.gerrit.extensions.api.projects.ProjectApi;
import com.google.gerrit.extensions.api.projects.ProjectInput;
+import com.google.gerrit.extensions.api.projects.SubmitRequirementApi;
import com.google.gerrit.extensions.api.projects.TagApi;
import com.google.gerrit.extensions.api.projects.TagInfo;
import com.google.gerrit.extensions.common.BatchLabelInput;
@@ -140,6 +141,7 @@
private final Provider<ListLabels> listLabels;
private final PostLabels postLabels;
private final LabelApiImpl.Factory labelApi;
+ private final SubmitRequirementApiImpl.Factory submitRequirementApi;
@AssistedInject
ProjectApiImpl(
@@ -179,6 +181,7 @@
Provider<ListLabels> listLabels,
PostLabels postLabels,
LabelApiImpl.Factory labelApi,
+ SubmitRequirementApiImpl.Factory submitRequirementApi,
@Assisted ProjectResource project) {
this(
permissionBackend,
@@ -218,6 +221,7 @@
listLabels,
postLabels,
labelApi,
+ submitRequirementApi,
null);
}
@@ -259,6 +263,7 @@
Provider<ListLabels> listLabels,
PostLabels postLabels,
LabelApiImpl.Factory labelApi,
+ SubmitRequirementApiImpl.Factory submitRequirementApi,
@Assisted String name) {
this(
permissionBackend,
@@ -298,6 +303,7 @@
listLabels,
postLabels,
labelApi,
+ submitRequirementApi,
name);
}
@@ -339,6 +345,7 @@
Provider<ListLabels> listLabels,
PostLabels postLabels,
LabelApiImpl.Factory labelApi,
+ SubmitRequirementApiImpl.Factory submitRequirementApi,
String name) {
this.permissionBackend = permissionBackend;
this.createProject = createProject;
@@ -378,6 +385,7 @@
this.listLabels = listLabels;
this.postLabels = postLabels;
this.labelApi = labelApi;
+ this.submitRequirementApi = submitRequirementApi;
}
@Override
@@ -746,6 +754,15 @@
}
@Override
+ public SubmitRequirementApi submitRequirement(String name) throws RestApiException {
+ try {
+ return submitRequirementApi.create(checkExists(), name);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot parse submit requirement", e);
+ }
+ }
+
+ @Override
public void labels(BatchLabelInput input) throws RestApiException {
try {
postLabels.apply(checkExists(), input);
diff --git a/java/com/google/gerrit/server/api/projects/ProjectsModule.java b/java/com/google/gerrit/server/api/projects/ProjectsModule.java
index 987c71f..9f7e1b4 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectsModule.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectsModule.java
@@ -29,5 +29,6 @@
factory(CommitApiImpl.Factory.class);
factory(DashboardApiImpl.Factory.class);
factory(LabelApiImpl.Factory.class);
+ factory(SubmitRequirementApiImpl.Factory.class);
}
}
diff --git a/java/com/google/gerrit/server/api/projects/SubmitRequirementApiImpl.java b/java/com/google/gerrit/server/api/projects/SubmitRequirementApiImpl.java
new file mode 100644
index 0000000..96bdc82
--- /dev/null
+++ b/java/com/google/gerrit/server/api/projects/SubmitRequirementApiImpl.java
@@ -0,0 +1,113 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.api.projects;
+
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+import static com.google.gerrit.server.project.ProjectCache.illegalState;
+
+import com.google.gerrit.extensions.api.projects.SubmitRequirementApi;
+import com.google.gerrit.extensions.common.SubmitRequirementInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementInput;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.SubmitRequirementResource;
+import com.google.gerrit.server.restapi.project.CreateSubmitRequirement;
+import com.google.gerrit.server.restapi.project.GetSubmitRequirement;
+import com.google.gerrit.server.restapi.project.SubmitRequirementsCollection;
+import com.google.gerrit.server.restapi.project.UpdateSubmitRequirement;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+public class SubmitRequirementApiImpl implements SubmitRequirementApi {
+ interface Factory {
+ SubmitRequirementApiImpl create(ProjectResource project, String name);
+ }
+
+ private final SubmitRequirementsCollection submitRequirements;
+ private final CreateSubmitRequirement createSubmitRequirement;
+ private final UpdateSubmitRequirement updateSubmitRequirement;
+ private final GetSubmitRequirement getSubmitRequirement;
+ private final String name;
+ private final ProjectCache projectCache;
+
+ private ProjectResource project;
+
+ @Inject
+ SubmitRequirementApiImpl(
+ SubmitRequirementsCollection submitRequirements,
+ CreateSubmitRequirement createSubmitRequirement,
+ UpdateSubmitRequirement updateSubmitRequirement,
+ GetSubmitRequirement getSubmitRequirement,
+ ProjectCache projectCache,
+ @Assisted ProjectResource project,
+ @Assisted String name) {
+ this.submitRequirements = submitRequirements;
+ this.createSubmitRequirement = createSubmitRequirement;
+ this.updateSubmitRequirement = updateSubmitRequirement;
+ this.getSubmitRequirement = getSubmitRequirement;
+ this.projectCache = projectCache;
+ this.project = project;
+ this.name = name;
+ }
+
+ @Override
+ public SubmitRequirementApi create(SubmitRequirementInput input) throws RestApiException {
+ try {
+ createSubmitRequirement.apply(project, IdString.fromDecoded(name), input);
+
+ // recreate project resource because project state was updated
+ project =
+ new ProjectResource(
+ projectCache
+ .get(project.getNameKey())
+ .orElseThrow(illegalState(project.getNameKey())),
+ project.getUser());
+
+ return this;
+ } catch (Exception e) {
+ throw asRestApiException("Cannot create submit requirement", e);
+ }
+ }
+
+ @Override
+ public SubmitRequirementInfo get() throws RestApiException {
+ try {
+ return getSubmitRequirement.apply(resource()).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot get submit requirement", e);
+ }
+ }
+
+ @Override
+ public SubmitRequirementInfo update(SubmitRequirementInput input) throws RestApiException {
+ try {
+ return updateSubmitRequirement.apply(resource(), input).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot update submit requirement", e);
+ }
+ }
+
+ @Override
+ public void delete() throws RestApiException {
+ /** TODO(ghareeb): implement */
+ }
+
+ private SubmitRequirementResource resource() throws RestApiException, PermissionBackendException {
+ return submitRequirements.parse(project, IdString.fromDecoded(name));
+ }
+}
diff --git a/java/com/google/gerrit/server/cache/PerThreadCache.java b/java/com/google/gerrit/server/cache/PerThreadCache.java
index 4270d1e..ef00b80 100644
--- a/java/com/google/gerrit/server/cache/PerThreadCache.java
+++ b/java/com/google/gerrit/server/cache/PerThreadCache.java
@@ -21,9 +21,7 @@
import com.google.common.collect.Maps;
import com.google.gerrit.common.Nullable;
import java.util.Map;
-import java.util.Optional;
import java.util.function.Supplier;
-import javax.servlet.http.HttpServletRequest;
/**
* Caches object instances for a request as {@link ThreadLocal} in the serving thread.
@@ -60,12 +58,6 @@
private static final int PER_THREAD_CACHE_SIZE = 25;
/**
- * Optional HTTP request associated with the per-thread cache, should the thread be associated
- * with the incoming HTTP thread pool.
- */
- private final Optional<HttpServletRequest> httpRequest;
-
- /**
* Unique key for key-value mappings stored in PerThreadCache. The key is based on the value's
* class and a list of identifiers that in combination uniquely set the object apart form others
* of the same class.
@@ -110,9 +102,9 @@
}
}
- public static PerThreadCache create(@Nullable HttpServletRequest httpRequest) {
+ public static PerThreadCache create() {
checkState(CACHE.get() == null, "called create() twice on the same request");
- PerThreadCache cache = new PerThreadCache(httpRequest);
+ PerThreadCache cache = new PerThreadCache();
CACHE.set(cache);
return cache;
}
@@ -129,9 +121,7 @@
private final Map<Key<?>, Object> cache = Maps.newHashMapWithExpectedSize(PER_THREAD_CACHE_SIZE);
- private PerThreadCache(@Nullable HttpServletRequest req) {
- httpRequest = Optional.ofNullable(req);
- }
+ private PerThreadCache() {}
/**
* Returns an instance of {@code T} that was either loaded from the cache or obtained from the
@@ -149,19 +139,6 @@
return value;
}
- /** Returns the optional HTTP request associated with the local thread cache. */
- public Optional<HttpServletRequest> getHttpRequest() {
- return httpRequest;
- }
-
- /** Returns true if there is an HTTP request associated and is a GET or HEAD */
- public boolean hasReadonlyRequest() {
- return httpRequest
- .map(HttpServletRequest::getMethod)
- .filter(m -> m.equalsIgnoreCase("GET") || m.equalsIgnoreCase("HEAD"))
- .isPresent();
- }
-
@Override
public void close() {
CACHE.remove();
diff --git a/java/com/google/gerrit/server/change/AddToAttentionSetOp.java b/java/com/google/gerrit/server/change/AddToAttentionSetOp.java
index 1cf31c1..1acc91d 100644
--- a/java/com/google/gerrit/server/change/AddToAttentionSetOp.java
+++ b/java/com/google/gerrit/server/change/AddToAttentionSetOp.java
@@ -20,6 +20,7 @@
import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.Change;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.extensions.events.AttentionSetObserver;
import com.google.gerrit.server.mail.send.AddToAttentionSetSender;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.query.change.ChangeData;
@@ -39,6 +40,7 @@
private final ChangeData.Factory changeDataFactory;
private final AddToAttentionSetSender.Factory addToAttentionSetSender;
private final AttentionSetEmail.Factory attentionSetEmailFactory;
+ private final AttentionSetObserver attentionSetObserver;
private final Account.Id attentionUserId;
private final String reason;
@@ -58,12 +60,14 @@
ChangeData.Factory changeDataFactory,
AddToAttentionSetSender.Factory addToAttentionSetSender,
AttentionSetEmail.Factory attentionSetEmailFactory,
+ AttentionSetObserver attentionSetObserver,
@Assisted Account.Id attentionUserId,
@Assisted String reason,
@Assisted boolean notify) {
this.changeDataFactory = changeDataFactory;
this.addToAttentionSetSender = addToAttentionSetSender;
this.attentionSetEmailFactory = attentionSetEmailFactory;
+ this.attentionSetObserver = attentionSetObserver;
this.attentionUserId = requireNonNull(attentionUserId, "user");
this.reason = requireNonNull(reason, "reason");
@@ -85,10 +89,13 @@
change = ctx.getChange();
- ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
- update.addToPlannedAttentionSetUpdates(
+ ChangeUpdate changeUpdate = ctx.getUpdate(ctx.getChange().currentPatchSetId());
+ AttentionSetUpdate attentionUpdate =
AttentionSetUpdate.createForWrite(
- attentionUserId, AttentionSetUpdate.Operation.ADD, reason));
+ attentionUserId, AttentionSetUpdate.Operation.ADD, reason);
+ changeUpdate.addToPlannedAttentionSetUpdates(attentionUpdate);
+ attentionSetObserver.fire(
+ changeDataFactory.create(change), ctx.getAccount(), attentionUpdate, ctx.getWhen());
return true;
}
diff --git a/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java b/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java
index 9fb4fc4..2305791 100644
--- a/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java
+++ b/java/com/google/gerrit/server/change/RemoveFromAttentionSetOp.java
@@ -21,6 +21,7 @@
import com.google.gerrit.entities.AttentionSetUpdate.Operation;
import com.google.gerrit.entities.Change;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.extensions.events.AttentionSetObserver;
import com.google.gerrit.server.mail.send.RemoveFromAttentionSetSender;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.query.change.ChangeData;
@@ -41,6 +42,7 @@
private final ChangeData.Factory changeDataFactory;
private final RemoveFromAttentionSetSender.Factory removeFromAttentionSetSender;
private final AttentionSetEmail.Factory attentionSetEmailFactory;
+ private final AttentionSetObserver attentionSetObserver;
private final Account.Id attentionUserId;
private final String reason;
@@ -60,12 +62,14 @@
ChangeData.Factory changeDataFactory,
RemoveFromAttentionSetSender.Factory removeFromAttentionSetSenderFactory,
AttentionSetEmail.Factory attentionSetEmailFactory,
+ AttentionSetObserver attentionSetObserver,
@Assisted Account.Id attentionUserId,
@Assisted String reason,
@Assisted boolean notify) {
this.changeDataFactory = changeDataFactory;
this.removeFromAttentionSetSender = removeFromAttentionSetSenderFactory;
this.attentionSetEmailFactory = attentionSetEmailFactory;
+ this.attentionSetObserver = attentionSetObserver;
this.attentionUserId = requireNonNull(attentionUserId, "user");
this.reason = requireNonNull(reason, "reason");
this.notify = notify;
@@ -86,9 +90,12 @@
change = ctx.getChange();
- ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
- update.addToPlannedAttentionSetUpdates(
- AttentionSetUpdate.createForWrite(attentionUserId, Operation.REMOVE, reason));
+ ChangeUpdate changeUpdate = ctx.getUpdate(ctx.getChange().currentPatchSetId());
+ AttentionSetUpdate attentionUpdate =
+ AttentionSetUpdate.createForWrite(attentionUserId, Operation.REMOVE, reason);
+ changeUpdate.addToPlannedAttentionSetUpdates(attentionUpdate);
+ attentionSetObserver.fire(
+ changeDataFactory.create(change), ctx.getAccount(), attentionUpdate, ctx.getWhen());
return true;
}
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index a750d8e..c6263e2 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -37,6 +37,7 @@
import com.google.gerrit.extensions.events.AccountIndexedListener;
import com.google.gerrit.extensions.events.AgreementSignupListener;
import com.google.gerrit.extensions.events.AssigneeChangedListener;
+import com.google.gerrit.extensions.events.AttentionSetListener;
import com.google.gerrit.extensions.events.ChangeAbandonedListener;
import com.google.gerrit.extensions.events.ChangeDeletedListener;
import com.google.gerrit.extensions.events.ChangeIndexedListener;
@@ -173,6 +174,7 @@
import com.google.gerrit.server.mail.send.MailSoyTemplateProvider;
import com.google.gerrit.server.mime.FileTypeRegistry;
import com.google.gerrit.server.mime.MimeUtilFileTypeRegistry;
+import com.google.gerrit.server.notedb.DeleteZombieCommentsRefs;
import com.google.gerrit.server.notedb.NoteDbModule;
import com.google.gerrit.server.notedb.StoreSubmitRequirementsOp;
import com.google.gerrit.server.patch.DiffOperationsImpl;
@@ -188,7 +190,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.SubmitRequirementConfigValidator;
import com.google.gerrit.server.project.SubmitRequirementsEvaluatorImpl;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.query.FileEditsPredicate;
@@ -312,6 +314,7 @@
bind(AccountDefaultDisplayName.class).toInstance(accountDefaultDisplayName);
factory(ProjectOwnerGroupsProvider.Factory.class);
factory(SubmitRuleEvaluator.Factory.class);
+ factory(DeleteZombieCommentsRefs.Factory.class);
bind(AuthBackend.class).to(UniversalAuthBackend.class).in(SINGLETON);
DynamicSet.setOf(binder(), AuthBackend.class);
@@ -397,7 +400,7 @@
DynamicSet.setOf(binder(), UserScopedEventListener.class);
DynamicSet.setOf(binder(), CommitValidationListener.class);
DynamicSet.bind(binder(), CommitValidationListener.class)
- .to(SubmitRequirementExpressionsValidator.class);
+ .to(SubmitRequirementConfigValidator.class);
DynamicSet.setOf(binder(), CommentValidator.class);
DynamicSet.setOf(binder(), ChangeMessageModifier.class);
DynamicSet.setOf(binder(), RefOperationValidationListener.class);
@@ -454,6 +457,7 @@
DynamicSet.setOf(binder(), MailSoyTemplateProvider.class);
DynamicSet.setOf(binder(), OnPostReview.class);
DynamicMap.mapOf(binder(), AccountTagProvider.class);
+ DynamicSet.setOf(binder(), AttentionSetListener.class);
DynamicMap.mapOf(binder(), MailFilter.class);
bind(MailFilter.class).annotatedWith(Exports.named("ListMailFilter")).to(ListMailFilter.class);
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index 232aa6a..2957d6b 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -53,10 +53,10 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.time.Instant;
+import java.time.ZoneId;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
-import java.util.TimeZone;
import org.eclipse.jgit.diff.DiffAlgorithm;
import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm;
import org.eclipse.jgit.diff.RawText;
@@ -91,7 +91,7 @@
@Singleton
public class ChangeEditModifier {
- private final TimeZone tz;
+ private final ZoneId zoneId;
private final Provider<CurrentUser> currentUser;
private final PermissionBackend permissionBackend;
private final ChangeEditUtil changeEditUtil;
@@ -110,12 +110,12 @@
ProjectCache projectCache) {
this.currentUser = currentUser;
this.permissionBackend = permissionBackend;
- this.tz = gerritIdent.getTimeZone();
+ this.zoneId = gerritIdent.getZoneId();
this.changeEditUtil = changeEditUtil;
this.patchSetUtil = patchSetUtil;
this.projectCache = projectCache;
- noteDbEdits = new NoteDbEdits(tz, indexer, currentUser);
+ noteDbEdits = new NoteDbEdits(zoneId, indexer, currentUser);
}
/**
@@ -519,7 +519,7 @@
private PersonIdent getCommitterIdent(Instant commitTimestamp) {
IdentifiedUser user = currentUser.get().asIdentifiedUser();
- return user.newCommitterIdent(commitTimestamp, tz);
+ return user.newCommitterIdent(commitTimestamp, zoneId);
}
/**
@@ -709,12 +709,12 @@
}
private static class NoteDbEdits {
- private final TimeZone tz;
+ private final ZoneId zoneId;
private final ChangeIndexer indexer;
private final Provider<CurrentUser> currentUser;
- NoteDbEdits(TimeZone tz, ChangeIndexer indexer, Provider<CurrentUser> currentUser) {
- this.tz = tz;
+ NoteDbEdits(ZoneId zoneId, ChangeIndexer indexer, Provider<CurrentUser> currentUser) {
+ this.zoneId = zoneId;
this.indexer = indexer;
this.currentUser = currentUser;
}
@@ -841,7 +841,7 @@
private PersonIdent getRefLogIdent(Instant timestamp) {
IdentifiedUser user = currentUser.get().asIdentifiedUser();
- return user.newRefLogIdent(timestamp, tz);
+ return user.newRefLogIdent(timestamp, zoneId);
}
private void reindex(Change change) {
diff --git a/java/com/google/gerrit/server/events/ProjectHeadUpdatedEvent.java b/java/com/google/gerrit/server/events/ProjectHeadUpdatedEvent.java
new file mode 100644
index 0000000..90ed285
--- /dev/null
+++ b/java/com/google/gerrit/server/events/ProjectHeadUpdatedEvent.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.events;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+
+public class ProjectHeadUpdatedEvent extends ProjectEvent {
+
+ static final String TYPE = "project-head-updated";
+
+ public String projectName;
+ public String oldHead;
+ public String newHead;
+
+ public ProjectHeadUpdatedEvent() {
+ super(TYPE);
+ }
+
+ @Override
+ public NameKey getProjectNameKey() {
+ return Project.nameKey(projectName);
+ }
+}
diff --git a/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index abacb85..afe2a7c 100644
--- a/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -39,6 +39,7 @@
import com.google.gerrit.extensions.events.CommentAddedListener;
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
import com.google.gerrit.extensions.events.HashtagsEditedListener;
+import com.google.gerrit.extensions.events.HeadUpdatedListener;
import com.google.gerrit.extensions.events.NewProjectCreatedListener;
import com.google.gerrit.extensions.events.PrivateStateChangedListener;
import com.google.gerrit.extensions.events.ReviewerAddedListener;
@@ -86,7 +87,8 @@
ReviewerDeletedListener,
RevisionCreatedListener,
TopicEditedListener,
- VoteDeletedListener {
+ VoteDeletedListener,
+ HeadUpdatedListener {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public static class StreamEventsApiListenerModule extends AbstractModule {
@@ -111,6 +113,7 @@
DynamicSet.bind(binder(), VoteDeletedListener.class).to(StreamEventsApiListener.class);
DynamicSet.bind(binder(), WorkInProgressStateChangedListener.class)
.to(StreamEventsApiListener.class);
+ DynamicSet.bind(binder(), HeadUpdatedListener.class).to(StreamEventsApiListener.class);
}
}
@@ -339,6 +342,16 @@
}
@Override
+ public void onHeadUpdated(HeadUpdatedListener.Event ev) {
+ ProjectHeadUpdatedEvent event = new ProjectHeadUpdatedEvent();
+ event.projectName = ev.getProjectName();
+ event.oldHead = ev.getOldHeadName();
+ event.newHead = ev.getNewHeadName();
+
+ dispatcher.run(d -> d.postEvent(event.getProjectNameKey(), event));
+ }
+
+ @Override
public void onHashtagsEdited(HashtagsEditedListener.Event ev) {
try {
ChangeNotes notes = getNotes(ev.getChange());
diff --git a/java/com/google/gerrit/server/extensions/events/AttentionSetObserver.java b/java/com/google/gerrit/server/extensions/events/AttentionSetObserver.java
new file mode 100644
index 0000000..8f51e13
--- /dev/null
+++ b/java/com/google/gerrit/server/extensions/events/AttentionSetObserver.java
@@ -0,0 +1,112 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.extensions.events;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.AttentionSetUpdate;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.events.AttentionSetListener;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.time.Instant;
+import java.util.HashSet;
+import java.util.Set;
+
+/** Helper class to fire an event when an attention set changes. */
+@Singleton
+public class AttentionSetObserver {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final PluginSetContext<AttentionSetListener> listeners;
+ private final EventUtil util;
+ private final AccountCache accountCache;
+
+ @Inject
+ AttentionSetObserver(
+ PluginSetContext<AttentionSetListener> listeners, EventUtil util, AccountCache accountCache) {
+ this.listeners = listeners;
+ this.util = util;
+ this.accountCache = accountCache;
+ }
+
+ /**
+ * Notify all listening plugins
+ *
+ * @param changeData is current data of the change
+ * @param accountState is the initiator of the change
+ * @param update is the update that caused the event
+ * @param when is the time of the event
+ */
+ public void fire(
+ ChangeData changeData, AccountState accountState, AttentionSetUpdate update, Instant when) {
+ if (listeners.isEmpty()) {
+ return;
+ }
+ AccountState target = accountCache.get(update.account()).get();
+
+ HashSet<Integer> added = new HashSet<>();
+ HashSet<Integer> removed = new HashSet<>();
+ switch (update.operation()) {
+ case ADD:
+ added.add(target.account().id().get());
+ break;
+ case REMOVE:
+ removed.add(target.account().id().get());
+ break;
+ }
+
+ try {
+ Event event =
+ new Event(
+ util.changeInfo(changeData), util.accountInfo(accountState), added, removed, when);
+ listeners.runEach(l -> l.onAttentionSetChanged(event));
+ } catch (Exception e) {
+ logger.atSevere().withCause(e).log("Exception while firing AttentionSet changed event");
+ }
+ }
+
+ /** Event to be fired when an attention set changes */
+ private static class Event extends AbstractChangeEvent implements AttentionSetListener.Event {
+ private final Set<Integer> added;
+ private final Set<Integer> removed;
+
+ Event(
+ ChangeInfo change,
+ AccountInfo editor,
+ Set<Integer> added,
+ Set<Integer> removed,
+ Instant when) {
+ super(change, editor, when, NotifyHandling.ALL);
+ this.added = added;
+ this.removed = removed;
+ }
+
+ @Override
+ public Set<Integer> usersAdded() {
+ return added;
+ }
+
+ @Override
+ public Set<Integer> usersRemoved() {
+ return removed;
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/git/BanCommit.java b/java/com/google/gerrit/server/git/BanCommit.java
index 9cc754c..e27197c 100644
--- a/java/com/google/gerrit/server/git/BanCommit.java
+++ b/java/com/google/gerrit/server/git/BanCommit.java
@@ -31,8 +31,8 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.time.Instant;
+import java.time.ZoneId;
import java.util.List;
-import java.util.TimeZone;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.Constants;
@@ -78,7 +78,7 @@
private final Provider<IdentifiedUser> currentUser;
private final GitRepositoryManager repoManager;
- private final TimeZone tz;
+ private final ZoneId zoneId;
private final PermissionBackend permissionBackend;
private final NotesBranchUtil.Factory notesBranchUtilFactory;
@@ -93,7 +93,7 @@
this.repoManager = repoManager;
this.notesBranchUtilFactory = notesBranchUtilFactory;
this.permissionBackend = permissionBackend;
- this.tz = gerritIdent.getTimeZone();
+ this.zoneId = gerritIdent.getZoneId();
}
/**
@@ -155,7 +155,7 @@
}
private PersonIdent createPersonIdent() {
- return currentUser.get().newCommitterIdent(Instant.now(), tz);
+ return currentUser.get().newCommitterIdent(Instant.now(), zoneId);
}
private static String buildCommitMessage(List<ObjectId> bannedCommits, String reason) {
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index b04fbf8..fa46bf4 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -222,7 +222,7 @@
PersonIdent committerIdent = serverIdent.get();
PersonIdent authorIdent =
- user.asIdentifiedUser().newCommitterIdent(ts, committerIdent.getTimeZone());
+ user.asIdentifiedUser().newCommitterIdent(ts, committerIdent.getZoneId());
RevCommit parentToCommitToRevert = commitToRevert.getParent(0);
revWalk.parseHeaders(parentToCommitToRevert);
diff --git a/java/com/google/gerrit/server/git/RefCache.java b/java/com/google/gerrit/server/git/RefCache.java
index 5a5cae9..2dee427 100644
--- a/java/com/google/gerrit/server/git/RefCache.java
+++ b/java/com/google/gerrit/server/git/RefCache.java
@@ -37,4 +37,7 @@
* present with a value of {@link ObjectId#zeroId()}.
*/
Optional<ObjectId> get(String refName) throws IOException;
+
+ /** Closes this cache, releasing the references to any underlying resources. */
+ void close();
}
diff --git a/java/com/google/gerrit/server/git/RepoRefCache.java b/java/com/google/gerrit/server/git/RepoRefCache.java
index 7f22111..d2b3c32 100644
--- a/java/com/google/gerrit/server/git/RepoRefCache.java
+++ b/java/com/google/gerrit/server/git/RepoRefCache.java
@@ -14,7 +14,6 @@
package com.google.gerrit.server.git;
-import com.google.gerrit.server.cache.PerThreadCache;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
@@ -29,19 +28,11 @@
public class RepoRefCache implements RefCache {
private final RefDatabase refdb;
private final Map<String, Optional<ObjectId>> ids;
-
- public static Optional<RefCache> getOptional(Repository repo) {
- PerThreadCache cache = PerThreadCache.get();
- if (cache != null && cache.hasReadonlyRequest()) {
- return Optional.of(
- cache.get(
- PerThreadCache.Key.create(RepoRefCache.class, repo), () -> new RepoRefCache(repo)));
- }
-
- return Optional.empty();
- }
+ private final Repository repo;
public RepoRefCache(Repository repo) {
+ repo.incrementOpen();
+ this.repo = repo;
this.refdb = repo.getRefDatabase();
this.ids = new HashMap<>();
}
@@ -62,4 +53,9 @@
public Map<String, Optional<ObjectId>> getCachedRefs() {
return Collections.unmodifiableMap(ids);
}
+
+ @Override
+ public void close() {
+ repo.close();
+ }
}
diff --git a/java/com/google/gerrit/server/group/db/AuditLogReader.java b/java/com/google/gerrit/server/group/db/AuditLogReader.java
index 3f7ef2c..4c1f69b 100644
--- a/java/com/google/gerrit/server/group/db/AuditLogReader.java
+++ b/java/com/google/gerrit/server/group/db/AuditLogReader.java
@@ -139,9 +139,6 @@
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()) {
@@ -169,7 +166,7 @@
return Optional.of(
new AutoValue_AuditLogReader_ParsedCommit(
authorId.get(),
- c.getAuthorIdent().getWhen().toInstant(),
+ c.getAuthorIdent().getWhenAsInstant(),
ImmutableList.copyOf(addedMembers),
ImmutableList.copyOf(removedMembers),
ImmutableList.copyOf(addedSubgroups),
diff --git a/java/com/google/gerrit/server/group/db/GroupConfig.java b/java/com/google/gerrit/server/group/db/GroupConfig.java
index 71cc08c..4f2c049 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfig.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfig.java
@@ -37,7 +37,6 @@
import java.io.IOException;
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;
@@ -300,9 +299,6 @@
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();
@@ -321,8 +317,8 @@
Instant commitTimestamp =
TimeUtil.truncateToSecond(
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)));
+ commit.setAuthor(new PersonIdent(commit.getAuthor(), commitTimestamp));
+ commit.setCommitter(new PersonIdent(commit.getCommitter(), commitTimestamp));
InternalGroup updatedGroup = updateGroup(commitTimestamp);
diff --git a/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java b/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
index 7029d10..2e75d46 100644
--- a/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
@@ -58,7 +58,7 @@
.add(AccountField.ID_STR)
.build();
- // Bump Lucene version requires reindexing
+ // Upgrade Lucene to 7.x requires reindexing.
static final Schema<AccountState> V12 = schema(V11);
/**
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index aa08069..ec1506d 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -118,9 +118,13 @@
new Schema.Builder<ChangeData>().add(V75).add(ChangeField.FOOTER_NAME).build();
/** Added new field {@link ChangeField#COMMIT_MESSAGE_EXACT}. */
+ @Deprecated
static final Schema<ChangeData> V77 =
new Schema.Builder<ChangeData>().add(V76).add(ChangeField.COMMIT_MESSAGE_EXACT).build();
+ // Upgrade Lucene to 7.x requires reindexing.
+ static final Schema<ChangeData> V78 = schema(V77);
+
/**
* Name of the change index to be used when contacting index backends or loading configurations.
*/
diff --git a/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java b/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
index 773aa9a..91dd285 100644
--- a/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
@@ -45,7 +45,10 @@
// New numeric types: use dimensional points using the k-d tree geo-spatial data structure
// to offer fast single- and multi-dimensional numeric range.
- static final Schema<InternalGroup> V8 = schema(V7);
+ @Deprecated static final Schema<InternalGroup> V8 = schema(V7);
+
+ // Upgrade Lucene to 7.x requires reindexing.
+ static final Schema<InternalGroup> V9 = schema(V8);
/** Singleton instance of the schema definitions. This is one per JVM. */
public static final GroupSchemaDefinitions INSTANCE = new GroupSchemaDefinitions();
diff --git a/java/com/google/gerrit/server/logging/Metadata.java b/java/com/google/gerrit/server/logging/Metadata.java
index 5cd0e98..b433e9f 100644
--- a/java/com/google/gerrit/server/logging/Metadata.java
+++ b/java/com/google/gerrit/server/logging/Metadata.java
@@ -102,6 +102,9 @@
/** The name of a group. */
public abstract Optional<String> groupName();
+ /** The group system being queried. */
+ public abstract Optional<String> groupSystem();
+
/** The UUID of a group. */
public abstract Optional<String> groupUuid();
@@ -328,6 +331,8 @@
public abstract Builder groupName(@Nullable String groupName);
+ public abstract Builder groupSystem(@Nullable String groupSystem);
+
public abstract Builder groupUuid(@Nullable String groupUuid);
public abstract Builder httpStatus(int httpStatus);
diff --git a/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java b/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
index ad1703d..8ee8fc2 100644
--- a/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
+++ b/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
@@ -14,8 +14,9 @@
package com.google.gerrit.server.mail.send;
+import static com.google.common.base.Preconditions.checkArgument;
+
import com.google.common.io.CharStreams;
-import com.google.common.io.Resources;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.inject.Inject;
@@ -26,6 +27,7 @@
import com.google.template.soy.shared.SoyAstCache;
import java.io.IOException;
import java.io.Reader;
+import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -137,6 +139,8 @@
}
// Otherwise load the template as a resource.
- builder.add(Resources.getResource(logicalPath), logicalPath);
+ URL resource = this.getClass().getClassLoader().getResource(logicalPath);
+ checkArgument(resource != null, "resource %s not found.", logicalPath);
+ builder.add(resource, logicalPath);
}
}
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index 7efda47..e6f1622 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -29,7 +29,6 @@
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;
import org.eclipse.jgit.lib.ObjectId;
@@ -56,9 +55,6 @@
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,
@@ -66,7 +62,7 @@
ChangeNoteUtil noteUtil,
Instant when) {
this.noteUtil = noteUtil;
- this.serverIdent = new PersonIdent(serverIdent, Date.from(when));
+ this.serverIdent = new PersonIdent(serverIdent, when);
this.notes = notes;
this.change = notes.getChange();
this.accountId = accountId(user);
@@ -76,9 +72,6 @@
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,
@@ -92,7 +85,7 @@
(notes != null && change == null) || (notes == null && change != null),
"exactly one of notes or change required");
this.noteUtil = noteUtil;
- this.serverIdent = new PersonIdent(serverIdent, Date.from(when));
+ this.serverIdent = new PersonIdent(serverIdent, when);
this.notes = notes;
this.change = change != null ? change : notes.getChange();
this.accountId = accountId;
@@ -213,9 +206,6 @@
* 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;
@@ -236,7 +226,7 @@
return null; // Impl is a no-op.
}
cb.setAuthor(authorIdent);
- cb.setCommitter(new PersonIdent(serverIdent, Date.from(when)));
+ cb.setCommitter(new PersonIdent(serverIdent, when));
setParentCommit(cb, curr);
if (cb.getTreeId() == null) {
if (curr.equals(z)) {
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteJson.java b/java/com/google/gerrit/server/notedb/ChangeNoteJson.java
index 69185b1..de401ac 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteJson.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteJson.java
@@ -20,6 +20,7 @@
import com.google.gerrit.entities.SubmitRequirementExpressionResult.Status;
import com.google.gerrit.json.EnumTypeAdapterFactory;
import com.google.gerrit.json.OptionalSubmitRequirementExpressionResultAdapterFactory;
+import com.google.gerrit.json.OptionalTypeAdapter;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
@@ -54,6 +55,7 @@
static Gson newGson() {
return new GsonBuilder()
+ .registerTypeAdapter(Optional.class, new OptionalTypeAdapter())
.registerTypeAdapter(Timestamp.class, new CommentTimestampAdapter().nullSafe())
.registerTypeAdapterFactory(new EnumTypeAdapterFactory())
.registerTypeAdapterFactory(EntitiesAdapterFactory.create())
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index e9d2f4c..6f413f1 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -24,7 +24,6 @@
import com.google.gson.Gson;
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;
@@ -100,16 +99,13 @@
* Returns a {@link PersonIdent} that contains the account ID, but not the user's name or email
* address.
*/
- // 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),
- Date.from(when),
- serverIdent.getTimeZone());
+ when,
+ serverIdent.getZoneId());
}
/** Returns the string {@code "Gerrit User " + accountId}, to pseudonymize user names. */
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 0e5cf11..cc4e9ce 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -57,7 +57,6 @@
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.git.RefCache;
-import com.google.gerrit.server.git.RepoRefCache;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.query.change.ChangeData;
@@ -688,10 +687,6 @@
@Override
protected ObjectId readRef(Repository repo) throws IOException {
- Optional<RefCache> refsCache =
- Optional.ofNullable(refs).map(Optional::of).orElse(RepoRefCache.getOptional(repo));
- return refsCache.isPresent()
- ? refsCache.get().get(getRefName()).orElse(null)
- : super.readRef(repo);
+ return refs != null ? refs.get(getRefName()).orElse(null) : super.readRef(repo);
}
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 1d8ec82..6700f25 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -1270,11 +1270,8 @@
* @param commit the commit to return commit time.
* @return the timestamp when the commit was applied.
*/
- // 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();
+ return commit.getCommitterIdent().getWhenAsInstant();
}
private void pruneReviewers() {
diff --git a/java/com/google/gerrit/server/notedb/CommitRewriter.java b/java/com/google/gerrit/server/notedb/CommitRewriter.java
index e7a8948..da20475 100644
--- a/java/com/google/gerrit/server/notedb/CommitRewriter.java
+++ b/java/com/google/gerrit/server/notedb/CommitRewriter.java
@@ -578,12 +578,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()
+ && newIdent.getWhenAsInstant().equals(originalIdent.getWhenAsInstant())
&& newIdent.getEmailAddress().equals(originalIdent.getEmailAddress());
}
diff --git a/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java b/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
index 28436db..2f17675 100644
--- a/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
+++ b/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
@@ -15,19 +15,30 @@
package com.google.gerrit.server.notedb;
import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.gerrit.entities.RefNames.REFS_DRAFT_COMMENTS;
+import static org.eclipse.jgit.lib.Constants.EMPTY_TREE_ID;
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.HumanComment;
import com.google.gerrit.git.RefUpdateUtil;
+import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
@@ -39,16 +50,20 @@
* href="https://gerrit-review.googlesource.com/c/gerrit/+/246233">
* https://gerrit-review.googlesource.com/c/gerrit/+/246233 </a>
*
- * <p>An earlier bug in the deletion of draft comments {@code
- * refs/draft-comments/$change_id_short/$change_id/$user_id} caused some draft refs to remain in Git
- * and not get deleted. These refs point to an empty tree.
+ * <p>The implementation has two cases for detecting zombie drafts:
+ *
+ * <ul>
+ * <li>An earlier bug in the deletion of draft comments {@code
+ * refs/draft-comments/$change_id_short/$change_id/$user_id} caused some draft refs to remain
+ * in Git and not get deleted. These refs point to an empty tree. We delete such refs.
+ * <li>Inspecting all draft-comment refs. Check for each draft if there exists a published comment
+ * with the same UUID. For now this runs in logging-only mode and does not remove these zombie
+ * drafts.
+ * </uL>
*/
public class DeleteZombieCommentsRefs {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- private static final String EMPTY_TREE_ID = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
- private static final String DRAFT_REFS_PREFIX = "refs/draft-comments";
-
// Number of refs deleted at once in a batch ref-update.
// Log progress after deleting every CHUNK_SIZE refs
private static final int CHUNK_SIZE = 3000;
@@ -56,8 +71,10 @@
private final GitRepositoryManager repoManager;
private final AllUsersName allUsers;
private final int cleanupPercentage;
- private Repository allUsersRepo;
private final Consumer<String> uiConsumer;
+ @Nullable private final DraftCommentNotes.Factory draftNotesFactory;
+ @Nullable private final ChangeNotes.Factory changeNotesFactory;
+ @Nullable private final CommentsUtil commentsUtil;
public interface Factory {
DeleteZombieCommentsRefs create(int cleanupPercentage);
@@ -67,8 +84,18 @@
public DeleteZombieCommentsRefs(
AllUsersName allUsers,
GitRepositoryManager repoManager,
+ ChangeNotes.Factory changeNotesFactory,
+ DraftCommentNotes.Factory draftNotesFactory,
+ CommentsUtil commentsUtil,
@Assisted Integer cleanupPercentage) {
- this(allUsers, repoManager, cleanupPercentage, (msg) -> {});
+ this(
+ allUsers,
+ repoManager,
+ cleanupPercentage,
+ (msg) -> {},
+ changeNotesFactory,
+ draftNotesFactory,
+ commentsUtil);
}
public DeleteZombieCommentsRefs(
@@ -76,43 +103,126 @@
GitRepositoryManager repoManager,
Integer cleanupPercentage,
Consumer<String> uiConsumer) {
+ this(allUsers, repoManager, cleanupPercentage, uiConsumer, null, null, null);
+ }
+
+ private DeleteZombieCommentsRefs(
+ AllUsersName allUsers,
+ GitRepositoryManager repoManager,
+ Integer cleanupPercentage,
+ Consumer<String> uiConsumer,
+ @Nullable ChangeNotes.Factory changeNotesFactory,
+ @Nullable DraftCommentNotes.Factory draftNotesFactory,
+ @Nullable CommentsUtil commentsUtil) {
this.allUsers = allUsers;
this.repoManager = repoManager;
this.cleanupPercentage = (cleanupPercentage == null) ? 100 : cleanupPercentage;
this.uiConsumer = uiConsumer;
+ this.draftNotesFactory = draftNotesFactory;
+ this.changeNotesFactory = changeNotesFactory;
+ this.commentsUtil = commentsUtil;
}
public void execute() throws IOException {
- allUsersRepo = repoManager.openRepository(allUsers);
-
- List<Ref> draftRefs = allUsersRepo.getRefDatabase().getRefsByPrefix(DRAFT_REFS_PREFIX);
- List<Ref> zombieRefs = filterZombieRefs(draftRefs);
-
- logInfo(
- String.format(
- "Found a total of %d zombie draft refs in %s repo.",
- zombieRefs.size(), allUsers.get()));
-
- logInfo(String.format("Cleanup percentage = %d", cleanupPercentage));
- zombieRefs =
- zombieRefs.stream()
- .filter(ref -> Change.Id.fromAllUsersRef(ref.getName()).get() % 100 < cleanupPercentage)
- .collect(toImmutableList());
- logInfo(String.format("Number of zombie refs to be cleaned = %d", zombieRefs.size()));
-
- long zombieRefsCnt = zombieRefs.size();
- long deletedRefsCnt = 0;
- long startTime = System.currentTimeMillis();
-
- for (List<Ref> refsBatch : Iterables.partition(zombieRefs, CHUNK_SIZE)) {
- deleteBatchZombieRefs(refsBatch);
- long elapsed = (System.currentTimeMillis() - startTime) / 1000;
- deletedRefsCnt += refsBatch.size();
- logProgress(deletedRefsCnt, zombieRefsCnt, elapsed);
+ deleteDraftRefsThatPointToEmptyTree();
+ if (draftNotesFactory != null) {
+ getNumberOfDraftsThatAreAlsoPublished();
}
}
- private void deleteBatchZombieRefs(List<Ref> refsBatch) throws IOException {
+ private void deleteDraftRefsThatPointToEmptyTree() throws IOException {
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers)) {
+ List<Ref> draftRefs = allUsersRepo.getRefDatabase().getRefsByPrefix(REFS_DRAFT_COMMENTS);
+ List<Ref> zombieRefs = filterZombieRefs(allUsersRepo, draftRefs);
+
+ logInfo(
+ String.format(
+ "Found a total of %d zombie draft refs in %s repo.",
+ zombieRefs.size(), allUsers.get()));
+
+ logInfo(String.format("Cleanup percentage = %d", cleanupPercentage));
+ zombieRefs =
+ zombieRefs.stream()
+ .filter(
+ ref -> Change.Id.fromAllUsersRef(ref.getName()).get() % 100 < cleanupPercentage)
+ .collect(toImmutableList());
+ logInfo(String.format("Number of zombie refs to be cleaned = %d", zombieRefs.size()));
+
+ long zombieRefsCnt = zombieRefs.size();
+ long deletedRefsCnt = 0;
+ long startTime = System.currentTimeMillis();
+
+ for (List<Ref> refsBatch : Iterables.partition(zombieRefs, CHUNK_SIZE)) {
+ deleteBatchZombieRefs(allUsersRepo, refsBatch);
+ long elapsed = (System.currentTimeMillis() - startTime) / 1000;
+ deletedRefsCnt += refsBatch.size();
+ logProgress(deletedRefsCnt, zombieRefsCnt, elapsed);
+ }
+ }
+ }
+
+ /**
+ * For each draft comment, check if there exists a published comment with the same UUID and log a
+ * warning if that's the case.
+ */
+ @VisibleForTesting
+ public int getNumberOfDraftsThatAreAlsoPublished() throws IOException {
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers)) {
+ List<Ref> draftRefs = allUsersRepo.getRefDatabase().getRefsByPrefix(REFS_DRAFT_COMMENTS);
+ int numZombies = 0;
+ Set<ChangeUserIDsPair> visitedSet = new HashSet<>();
+ for (Ref draftRef : draftRefs) {
+ try {
+ Change.Id changeId = Change.Id.fromAllUsersRef(draftRef.getName());
+ Account.Id accountId = Account.Id.fromRef(draftRef.getName());
+ ChangeUserIDsPair changeUserIDsPair = ChangeUserIDsPair.create(changeId, accountId);
+ if (!visitedSet.add(changeUserIDsPair)) {
+ continue;
+ }
+ DraftCommentNotes draftNotes = draftNotesFactory.create(changeId, accountId).load();
+ ChangeNotes notes = changeNotesFactory.createCheckedUsingIndexLookup(changeId);
+ Set<String> draftIds = toUuid(draftNotes.getComments().values().asList());
+ Set<String> publishedIds = toUuid(commentsUtil.publishedHumanCommentsByChange(notes));
+ List<String> zombieIds =
+ draftIds.stream()
+ .filter(zombieId -> publishedIds.contains(zombieId))
+ .collect(Collectors.toList());
+ zombieIds.forEach(
+ zombieId ->
+ logger.atWarning().log(
+ "Draft comment with uuid '%s' of change %s"
+ + " is a zombie draft that is already published.",
+ zombieId, changeId));
+ numZombies += zombieIds.size();
+ } catch (Exception e) {
+ logger.atWarning().withCause(e).log("Failed to process ref %s", draftRef.getName());
+ }
+ }
+ if (numZombies > 0) {
+ logger.atWarning().log("Detected %d additional zombie drafts.", numZombies);
+ }
+ return numZombies;
+ }
+ }
+
+ @AutoValue
+ abstract static class ChangeUserIDsPair {
+ abstract Change.Id changeId();
+
+ abstract Account.Id accountId();
+
+ static ChangeUserIDsPair create(Change.Id changeId, Account.Id accountId) {
+ return new AutoValue_DeleteZombieCommentsRefs_ChangeUserIDsPair(changeId, accountId);
+ }
+ }
+
+ /** Map the list of input comments to their UUIDs. */
+ private Set<String> toUuid(List<HumanComment> in) {
+ return in.stream().map(c -> c.key.uuid).collect(Collectors.toSet());
+ }
+
+ private void deleteBatchZombieRefs(Repository allUsersRepo, List<Ref> refsBatch)
+ throws IOException {
List<ReceiveCommand> deleteCommands =
refsBatch.stream()
.map(
@@ -126,18 +236,19 @@
RefUpdateUtil.executeChecked(bru, allUsersRepo);
}
- private List<Ref> filterZombieRefs(List<Ref> allDraftRefs) throws IOException {
+ private List<Ref> filterZombieRefs(Repository allUsersRepo, List<Ref> allDraftRefs)
+ throws IOException {
List<Ref> zombieRefs = new ArrayList<>((int) (allDraftRefs.size() * 0.5));
for (Ref ref : allDraftRefs) {
- if (isZombieRef(ref)) {
+ if (isZombieRef(allUsersRepo, ref)) {
zombieRefs.add(ref);
}
}
return zombieRefs;
}
- private boolean isZombieRef(Ref ref) throws IOException {
- return allUsersRepo.parseCommit(ref.getObjectId()).getTree().getName().equals(EMPTY_TREE_ID);
+ private boolean isZombieRef(Repository allUsersRepo, Ref ref) throws IOException {
+ return allUsersRepo.parseCommit(ref.getObjectId()).getTree().getId().equals(EMPTY_TREE_ID);
}
private void logInfo(String message) {
diff --git a/java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java b/java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java
index 506d292..12a7841 100644
--- a/java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java
+++ b/java/com/google/gerrit/server/permissions/GitVisibleChangeFilter.java
@@ -130,11 +130,10 @@
notesResult -> {
if (!notesResult.error().isPresent()) {
return changeDataFactory.create(notesResult.notes());
- } else {
- logger.atWarning().withCause(notesResult.error().get()).log(
- "Unable to load ChangeNotes for %s", notesResult.id());
- return null;
}
+ logger.atWarning().withCause(notesResult.error().get()).log(
+ "Unable to load ChangeNotes for %s", notesResult.id());
+ return null;
})
.filter(Objects::nonNull);
} catch (IOException e) {
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index d816d84..7ace1c8 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -550,6 +550,7 @@
return submitRequirementSections;
}
+ /** Adds or replaces the given {@link SubmitRequirement} in this config. */
public void upsertSubmitRequirement(SubmitRequirement requirement) {
submitRequirementSections.put(requirement.name(), requirement);
}
@@ -1018,7 +1019,7 @@
continue;
}
- // The expressions are validated in SubmitRequirementExpressionsValidator.
+ // The expressions are validated in SubmitRequirementConfigValidator.
SubmitRequirement submitRequirement =
SubmitRequirement.builder()
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementConfigValidator.java b/java/com/google/gerrit/server/project/SubmitRequirementConfigValidator.java
new file mode 100644
index 0000000..faca446
--- /dev/null
+++ b/java/com/google/gerrit/server/project/SubmitRequirementConfigValidator.java
@@ -0,0 +1,135 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.SubmitRequirement;
+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.List;
+import java.util.stream.Collectors;
+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 SubmitRequirementConfigValidator implements CommitValidationListener {
+ private final DiffOperations diffOperations;
+ private final ProjectConfig.Factory projectConfigFactory;
+ private final SubmitRequirementExpressionsValidator submitRequirementExpressionsValidator;
+
+ @Inject
+ SubmitRequirementConfigValidator(
+ DiffOperations diffOperations,
+ ProjectConfig.Factory projectConfigFactory,
+ SubmitRequirementExpressionsValidator submitRequirementExpressionsValidator) {
+ this.diffOperations = diffOperations;
+ this.projectConfigFactory = projectConfigFactory;
+ this.submitRequirementExpressionsValidator = submitRequirementExpressionsValidator;
+ }
+
+ @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.Builder<String> validationMsgsBuilder = ImmutableList.builder();
+ for (SubmitRequirement submitRequirement :
+ projectConfig.getSubmitRequirementSections().values()) {
+ validationMsgsBuilder.addAll(
+ submitRequirementExpressionsValidator.validateExpressions(submitRequirement));
+ }
+ ImmutableList<String> validationMsgs = validationMsgsBuilder.build();
+ if (!validationMsgs.isEmpty()) {
+ throw new CommitValidationException(
+ String.format(
+ "invalid submit requirement expressions in %s (revision = %s)",
+ ProjectConfig.PROJECT_CONFIG, projectConfig.getRevision()),
+ new ImmutableList.Builder<CommitValidationMessage>()
+ .add(
+ new CommitValidationMessage(
+ "Invalid project configuration", ValidationMessage.Type.ERROR))
+ .addAll(
+ validationMsgs.stream()
+ .map(m -> toCommitValidationMessage(m))
+ .collect(Collectors.toList()))
+ .build());
+ }
+ 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);
+ }
+ }
+
+ private static CommitValidationMessage toCommitValidationMessage(String message) {
+ return new CommitValidationMessage(message, ValidationMessage.Type.ERROR);
+ }
+
+ /**
+ * 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;
+ }
+}
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementExpressionsValidator.java b/java/com/google/gerrit/server/project/SubmitRequirementExpressionsValidator.java
index 8717581..f2e4ff8 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementExpressionsValidator.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementExpressionsValidator.java
@@ -15,144 +15,59 @@
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 com.google.inject.Singleton;
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;
+@Singleton
+public class SubmitRequirementExpressionsValidator {
private final SubmitRequirementsEvaluator submitRequirementsEvaluator;
@Inject
- SubmitRequirementExpressionsValidator(
- DiffOperations diffOperations,
- ProjectConfig.Factory projectConfigFactory,
- SubmitRequirementsEvaluator submitRequirementsEvaluator) {
- this.diffOperations = diffOperations;
- this.projectConfigFactory = projectConfigFactory;
+ SubmitRequirementExpressionsValidator(SubmitRequirementsEvaluator submitRequirementsEvaluator) {
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.
+ * Validates the query expressions on the input {@code submitRequirement}.
*
- * @param receiveEvent the receive event
- * @param fileName the name of the file
+ * @return list of string containing the error messages resulting from the validation. The list is
+ * empty if the "submit requirement" is valid.
*/
- 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));
- }
+ public ImmutableList<String> validateExpressions(SubmitRequirement submitRequirement) {
+ List<String> validationMessages = new ArrayList<>();
+ 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,
+ List<String> validationMessages,
SubmitRequirement submitRequirement,
SubmitRequirementExpression expression,
String configKey) {
@@ -160,23 +75,19 @@
submitRequirementsEvaluator.validateExpression(expression);
} catch (QueryParseException e) {
if (validationMessages.isEmpty()) {
- validationMessages.add(
- new CommitValidationMessage(
- "Invalid project configuration", ValidationMessage.Type.ERROR));
+ validationMessages.add("Invalid project configuration");
}
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));
+ 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()));
}
}
}
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementJson.java b/java/com/google/gerrit/server/project/SubmitRequirementJson.java
new file mode 100644
index 0000000..5593ff4
--- /dev/null
+++ b/java/com/google/gerrit/server/project/SubmitRequirementJson.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.extensions.common.SubmitRequirementInfo;
+import com.google.inject.Singleton;
+
+/** Converts a {@link SubmitRequirement} to a {@link SubmitRequirementInfo}. */
+@Singleton
+public class SubmitRequirementJson {
+ public static SubmitRequirementInfo format(SubmitRequirement sr) {
+ SubmitRequirementInfo info = new SubmitRequirementInfo();
+ info.name = sr.name();
+ info.description = sr.description().orElse(null);
+ if (sr.applicabilityExpression().isPresent()) {
+ info.applicabilityExpression = sr.applicabilityExpression().get().expressionString();
+ }
+ if (sr.overrideExpression().isPresent()) {
+ info.overrideExpression = sr.overrideExpression().get().expressionString();
+ }
+ info.submittabilityExpression = sr.submittabilityExpression().expressionString();
+ info.allowOverrideInChildProjects = sr.allowOverrideInChildProjects();
+ return info;
+ }
+}
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementResource.java b/java/com/google/gerrit/server/project/SubmitRequirementResource.java
new file mode 100644
index 0000000..d075cd7
--- /dev/null
+++ b/java/com/google/gerrit/server/project/SubmitRequirementResource.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+public class SubmitRequirementResource implements RestResource {
+ public static final TypeLiteral<RestView<SubmitRequirementResource>> SUBMIT_REQUIREMENT_KIND =
+ new TypeLiteral<>() {};
+
+ private final ProjectResource project;
+ private final SubmitRequirement submitRequirement;
+
+ public SubmitRequirementResource(ProjectResource project, SubmitRequirement submitRequirement) {
+ this.project = project;
+ this.submitRequirement = submitRequirement;
+ }
+
+ public ProjectResource getProject() {
+ return project;
+ }
+
+ public SubmitRequirement getSubmitRequirement() {
+ return submitRequirement;
+ }
+}
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java b/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java
index c234c8c..3789c42 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsUtil.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.project;
import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.metrics.Counter2;
@@ -29,6 +30,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
@@ -38,6 +40,12 @@
@Singleton
public class SubmitRequirementsUtil {
+ /**
+ * Submit requirement name can only contain alphanumeric characters or hyphen. Name cannot start
+ * with a hyphen or number.
+ */
+ private static final Pattern SUBMIT_REQ_NAME_PATTERN = Pattern.compile("[a-zA-Z][a-zA-Z0-9\\-]*");
+
@Singleton
static class Metrics {
final Counter2<String, String> submitRequirementsMatchingWithLegacy;
@@ -179,6 +187,20 @@
return ImmutableMap.copyOf(result);
}
+ /** Validates the name of submit requirements. */
+ public static void validateName(@Nullable String name) throws IllegalArgumentException {
+ if (name == null || name.isEmpty()) {
+ throw new IllegalArgumentException("Empty submit requirement name");
+ }
+ if (!SUBMIT_REQ_NAME_PATTERN.matcher(name).matches()) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Illegal submit requirement name \"%s\". Name can only consist of "
+ + "alphanumeric characters and '-'. Name cannot start with '-' or number.",
+ name));
+ }
+ }
+
private static boolean shouldReportMetric(ChangeData cd) {
// We only care about recording differences in old and new requirements for open changes
// that did not have their data retrieved from the (potentially stale) change index.
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index 232cd77..00c48dc 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -70,11 +70,11 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.time.Instant;
+import java.time.ZoneId;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.TimeZone;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.InvalidObjectIdException;
import org.eclipse.jgit.errors.MissingObjectException;
@@ -104,7 +104,7 @@
private final Sequences seq;
private final Provider<InternalChangeQuery> queryProvider;
private final GitRepositoryManager gitManager;
- private final TimeZone serverTimeZone;
+ private final ZoneId serverZoneId;
private final Provider<IdentifiedUser> user;
private final ChangeInserter.Factory changeInserterFactory;
private final PatchSetInserter.Factory patchSetInserterFactory;
@@ -135,7 +135,7 @@
this.seq = seq;
this.queryProvider = queryProvider;
this.gitManager = gitManager;
- this.serverTimeZone = myIdent.getTimeZone();
+ this.serverZoneId = myIdent.getZoneId();
this.user = user;
this.changeInserterFactory = changeInserterFactory;
this.patchSetInserterFactory = patchSetInserterFactory;
@@ -307,7 +307,7 @@
CodeReviewCommit cherryPickCommit;
ProjectState projectState =
projectCache.get(dest.project()).orElseThrow(noSuchProject(dest.project()));
- PersonIdent committerIdent = identifiedUser.newCommitterIdent(timestamp, serverTimeZone);
+ PersonIdent committerIdent = identifiedUser.newCommitterIdent(timestamp, serverZoneId);
try {
MergeUtil mergeUtil;
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index e668891..8c56967 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -86,11 +86,10 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.time.Instant;
+import java.time.ZoneId;
import java.util.Collections;
-import java.util.Date;
import java.util.List;
import java.util.Optional;
-import java.util.TimeZone;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.InvalidObjectIdException;
import org.eclipse.jgit.errors.MissingObjectException;
@@ -118,7 +117,7 @@
private final String anonymousCowardName;
private final GitRepositoryManager gitManager;
private final Sequences seq;
- private final TimeZone serverTimeZone;
+ private final ZoneId serverZoneId;
private final PermissionBackend permissionBackend;
private final Provider<CurrentUser> user;
private final ProjectsCollection projectsCollection;
@@ -158,7 +157,7 @@
this.anonymousCowardName = anonymousCowardName;
this.gitManager = gitManager;
this.seq = seq;
- this.serverTimeZone = myIdent.getTimeZone();
+ this.serverZoneId = myIdent.getZoneId();
this.permissionBackend = permissionBackend;
this.user = user;
this.projectsCollection = projectsCollection;
@@ -321,9 +320,6 @@
}
}
- // 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,
@@ -360,12 +356,11 @@
Instant now = TimeUtil.now();
- PersonIdent committer = me.newCommitterIdent(now, serverTimeZone);
+ PersonIdent committer = me.newCommitterIdent(now, serverZoneId);
PersonIdent author =
input.author == null
? committer
- : new PersonIdent(
- input.author.name, input.author.email, Date.from(now), serverTimeZone);
+ : new PersonIdent(input.author.name, input.author.email, now, serverZoneId);
String commitMessage = getCommitMessage(input.subject, me);
diff --git a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
index 7259deb..4b66cdc 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
@@ -69,9 +69,8 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.time.Instant;
-import java.util.Date;
+import java.time.ZoneId;
import java.util.List;
-import java.util.TimeZone;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
@@ -86,7 +85,7 @@
private final BatchUpdate.Factory updateFactory;
private final GitRepositoryManager gitManager;
private final CommitsCollection commits;
- private final TimeZone serverTimeZone;
+ private final ZoneId serverZoneId;
private final Provider<CurrentUser> user;
private final ChangeJson.Factory jsonFactory;
private final PatchSetUtil psUtil;
@@ -113,7 +112,7 @@
this.updateFactory = updateFactory;
this.gitManager = gitManager;
this.commits = commits;
- this.serverTimeZone = myIdent.getTimeZone();
+ this.serverZoneId = myIdent.getZoneId();
this.user = user;
this.jsonFactory = json;
this.psUtil = psUtil;
@@ -124,9 +123,6 @@
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 {
@@ -185,8 +181,8 @@
IdentifiedUser me = user.get().asIdentifiedUser();
PersonIdent author =
in.author == null
- ? me.newCommitterIdent(now, serverTimeZone)
- : new PersonIdent(in.author.name, in.author.email, Date.from(now), serverTimeZone);
+ ? me.newCommitterIdent(now, serverZoneId)
+ : new PersonIdent(in.author.name, in.author.email, now, serverZoneId);
CodeReviewCommit newCommit =
createMergeCommit(
in,
diff --git a/java/com/google/gerrit/server/restapi/change/PutMessage.java b/java/com/google/gerrit/server/restapi/change/PutMessage.java
index c62200a..f898dca 100644
--- a/java/com/google/gerrit/server/restapi/change/PutMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/PutMessage.java
@@ -48,7 +48,7 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.time.Instant;
-import java.util.TimeZone;
+import java.time.ZoneId;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.ObjectId;
@@ -64,7 +64,7 @@
private final BatchUpdate.Factory updateFactory;
private final GitRepositoryManager repositoryManager;
private final Provider<CurrentUser> userProvider;
- private final TimeZone tz;
+ private final ZoneId zoneId;
private final PatchSetInserter.Factory psInserterFactory;
private final PermissionBackend permissionBackend;
private final PatchSetUtil psUtil;
@@ -86,7 +86,7 @@
this.repositoryManager = repositoryManager;
this.userProvider = userProvider;
this.psInserterFactory = psInserterFactory;
- this.tz = gerritIdent.getTimeZone();
+ this.zoneId = gerritIdent.getZoneId();
this.permissionBackend = permissionBackend;
this.psUtil = psUtil;
this.notifyResolver = notifyResolver;
@@ -167,7 +167,8 @@
builder.setTreeId(basePatchSetCommit.getTree());
builder.setParentIds(basePatchSetCommit.getParents());
builder.setAuthor(basePatchSetCommit.getAuthorIdent());
- builder.setCommitter(userProvider.get().asIdentifiedUser().newCommitterIdent(timestamp, tz));
+ builder.setCommitter(
+ userProvider.get().asIdentifiedUser().newCommitterIdent(timestamp, zoneId));
builder.setMessage(commitMessage);
ObjectId newCommitId = objectInserter.insert(builder);
objectInserter.flush();
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index 842f4b9..f991e17 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -32,6 +32,7 @@
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.query.FieldBundle;
@@ -235,7 +236,10 @@
return suggestedReviewers;
}
- private static Account.Id fromIdField(FieldBundle f) {
+ private static Account.Id fromIdField(FieldBundle f, boolean useLegacyNumericFields) {
+ if (useLegacyNumericFields) {
+ return Account.id(f.getValue(AccountField.ID).intValue());
+ }
return Account.id(Integer.valueOf(f.getValue(AccountField.ID_STR)));
}
@@ -251,6 +255,10 @@
accountQueryBuilder.defaultQuery(suggestReviewers.getQuery()));
logger.atFine().log("accounts index query: %s", pred);
accountIndexRewriter.validateMaxTermsInQuery(pred);
+ boolean useLegacyNumericFields =
+ accountIndexes.getSearchIndex().getSchema().hasField(AccountField.ID);
+ FieldDef<AccountState, ?> idField =
+ useLegacyNumericFields ? AccountField.ID : AccountField.ID_STR;
ResultSet<FieldBundle> result =
accountIndexes
.getSearchIndex()
@@ -260,10 +268,12 @@
indexConfig,
0,
suggestReviewers.getLimit(),
- ImmutableSet.of(AccountField.ID_STR.getName())))
+ ImmutableSet.of(idField.getName())))
.readRaw();
List<Account.Id> matches =
- result.toList().stream().map(f -> fromIdField(f)).collect(toList());
+ result.toList().stream()
+ .map(f -> fromIdField(f, useLegacyNumericFields))
+ .collect(toList());
logger.atFine().log("Matches: %s", matches);
return matches;
} catch (TooManyTermsInQueryException e) {
diff --git a/java/com/google/gerrit/server/restapi/change/Revisions.java b/java/com/google/gerrit/server/restapi/change/Revisions.java
index 41fecaf..1d5064e 100644
--- a/java/com/google/gerrit/server/restapi/change/Revisions.java
+++ b/java/com/google/gerrit/server/restapi/change/Revisions.java
@@ -154,9 +154,6 @@
return ImmutableList.of();
}
- // 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());
@@ -167,7 +164,7 @@
.id(PatchSet.id(change.getId(), 0))
.commitId(editCommit)
.uploader(change.getUser().getAccountId())
- .createdOn(editCommit.getCommitterIdent().getWhen().toInstant())
+ .createdOn(editCommit.getCommitterIdent().getWhenAsInstant())
.build();
if (commitId == null || editCommit.equals(commitId)) {
return ImmutableList.of(new RevisionResource(change, ps, edit));
diff --git a/java/com/google/gerrit/server/restapi/group/CreateGroup.java b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
index f257f86..e617931 100644
--- a/java/com/google/gerrit/server/restapi/group/CreateGroup.java
+++ b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
@@ -61,13 +61,13 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
+import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
-import java.util.TimeZone;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.PersonIdent;
@@ -77,7 +77,7 @@
public class CreateGroup
implements RestCollectionCreateView<TopLevelResource, GroupResource, GroupInput> {
private final Provider<IdentifiedUser> self;
- private final TimeZone serverTimeZone;
+ private final ZoneId serverZoneId;
private final Provider<GroupsUpdate> groupsUpdateProvider;
private final GroupCache groupCache;
private final GroupResolver groups;
@@ -102,7 +102,7 @@
@GerritServerConfig Config cfg,
Sequences sequences) {
this.self = self;
- this.serverTimeZone = serverIdent.get().getTimeZone();
+ this.serverZoneId = serverIdent.get().getZoneId();
this.groupsUpdateProvider = groupsUpdateProvider;
this.groupCache = groupCache;
this.groups = groups;
@@ -212,7 +212,7 @@
createGroupArgs.uuid,
GroupUuid.make(
createGroupArgs.getGroupName(),
- self.get().newCommitterIdent(TimeUtil.now(), serverTimeZone)));
+ self.get().newCommitterIdent(TimeUtil.now(), serverZoneId)));
InternalGroupCreation groupCreation =
InternalGroupCreation.builder()
.setGroupUUID(uuid)
diff --git a/java/com/google/gerrit/server/restapi/project/CreateSubmitRequirement.java b/java/com/google/gerrit/server/restapi/project/CreateSubmitRequirement.java
new file mode 100644
index 0000000..2aeba89
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/CreateSubmitRequirement.java
@@ -0,0 +1,168 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.project;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementExpression;
+import com.google.gerrit.extensions.common.SubmitRequirementInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.SubmitRequirementExpressionsValidator;
+import com.google.gerrit.server.project.SubmitRequirementJson;
+import com.google.gerrit.server.project.SubmitRequirementResource;
+import com.google.gerrit.server.project.SubmitRequirementsUtil;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/** A rest create view that creates a "submit requirement" for a project. */
+@Singleton
+public class CreateSubmitRequirement
+ implements RestCollectionCreateView<
+ ProjectResource, SubmitRequirementResource, SubmitRequirementInput> {
+ private final Provider<CurrentUser> user;
+ private final PermissionBackend permissionBackend;
+ private final MetaDataUpdate.User updateFactory;
+ private final ProjectConfig.Factory projectConfigFactory;
+ private final ProjectCache projectCache;
+ private final SubmitRequirementExpressionsValidator submitRequirementExpressionsValidator;
+
+ @Inject
+ public CreateSubmitRequirement(
+ Provider<CurrentUser> user,
+ PermissionBackend permissionBackend,
+ MetaDataUpdate.User updateFactory,
+ ProjectConfig.Factory projectConfigFactory,
+ ProjectCache projectCache,
+ SubmitRequirementExpressionsValidator submitRequirementExpressionsValidator) {
+ this.user = user;
+ this.permissionBackend = permissionBackend;
+ this.updateFactory = updateFactory;
+ this.projectConfigFactory = projectConfigFactory;
+ this.projectCache = projectCache;
+ this.submitRequirementExpressionsValidator = submitRequirementExpressionsValidator;
+ }
+
+ @Override
+ public Response<SubmitRequirementInfo> apply(
+ ProjectResource rsrc, IdString id, SubmitRequirementInput input)
+ throws AuthException, BadRequestException, IOException, PermissionBackendException {
+ if (!user.get().isIdentifiedUser()) {
+ throw new AuthException("Authentication required");
+ }
+
+ permissionBackend
+ .currentUser()
+ .project(rsrc.getNameKey())
+ .check(ProjectPermission.WRITE_CONFIG);
+
+ if (input == null) {
+ input = new SubmitRequirementInput();
+ }
+
+ if (input.name != null && !input.name.equals(id.get())) {
+ throw new BadRequestException("name in input must match name in URL");
+ }
+
+ try (MetaDataUpdate md = updateFactory.create(rsrc.getNameKey())) {
+ ProjectConfig config = projectConfigFactory.read(md);
+
+ SubmitRequirement submitRequirement = createSubmitRequirement(config, id.get(), input);
+
+ md.setMessage(String.format("Create Submit Requirement %s", submitRequirement.name()));
+ config.commit(md);
+
+ projectCache.evict(rsrc.getProjectState().getProject().getNameKey());
+
+ return Response.created(SubmitRequirementJson.format(submitRequirement));
+ } catch (ConfigInvalidException e) {
+ throw new IOException("Failed to read project config", e);
+ } catch (ResourceConflictException e) {
+ throw new BadRequestException("Failed to create submit requirement", e);
+ }
+ }
+
+ public SubmitRequirement createSubmitRequirement(
+ ProjectConfig config, String name, SubmitRequirementInput input)
+ throws BadRequestException, ResourceConflictException {
+ validateSRName(name);
+ ensureSRUnique(name, config);
+ if (Strings.isNullOrEmpty(input.submittabilityExpression)) {
+ throw new BadRequestException("submittability_expression is required");
+ }
+ if (input.allowOverrideInChildProjects == null) {
+ // default is false
+ input.allowOverrideInChildProjects = false;
+ }
+ SubmitRequirement submitRequirement =
+ SubmitRequirement.builder()
+ .setName(name)
+ .setDescription(Optional.ofNullable(input.description))
+ .setApplicabilityExpression(
+ SubmitRequirementExpression.of(input.applicabilityExpression))
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create(input.submittabilityExpression))
+ .setOverrideExpression(SubmitRequirementExpression.of(input.overrideExpression))
+ .setAllowOverrideInChildProjects(input.allowOverrideInChildProjects)
+ .build();
+
+ List<String> validationMessages =
+ submitRequirementExpressionsValidator.validateExpressions(submitRequirement);
+ if (!validationMessages.isEmpty()) {
+ throw new BadRequestException(
+ String.format("Invalid submit requirement input: %s", validationMessages));
+ }
+
+ config.upsertSubmitRequirement(submitRequirement);
+ return submitRequirement;
+ }
+
+ private void validateSRName(String name) throws BadRequestException {
+ try {
+ SubmitRequirementsUtil.validateName(name);
+ } catch (IllegalArgumentException e) {
+ throw new BadRequestException(e.getMessage(), e);
+ }
+ }
+
+ private void ensureSRUnique(String name, ProjectConfig config) throws ResourceConflictException {
+ for (String srName : config.getSubmitRequirementSections().keySet()) {
+ if (srName.equalsIgnoreCase(name)) {
+ throw new ResourceConflictException(
+ String.format(
+ "submit requirement \"%s\" conflicts with existing submit requirement \"%s\"",
+ name, srName));
+ }
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/CreateTag.java b/java/com/google/gerrit/server/restapi/project/CreateTag.java
index 6980006..c5013f7 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateTag.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateTag.java
@@ -44,7 +44,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.TimeZone;
+import java.time.ZoneId;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.TagCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
@@ -136,7 +136,7 @@
resource
.getUser()
.asIdentifiedUser()
- .newCommitterIdent(TimeUtil.now(), TimeZone.getDefault()));
+ .newCommitterIdent(TimeUtil.now(), ZoneId.systemDefault()));
}
Ref result = tag.call();
diff --git a/java/com/google/gerrit/server/restapi/project/GetReflog.java b/java/com/google/gerrit/server/restapi/project/GetReflog.java
index e0131ee..967b3c5 100644
--- a/java/com/google/gerrit/server/restapi/project/GetReflog.java
+++ b/java/com/google/gerrit/server/restapi/project/GetReflog.java
@@ -89,9 +89,6 @@
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 {
@@ -118,7 +115,7 @@
} else {
entries = limit > 0 ? new ArrayList<>(limit) : new ArrayList<>();
for (ReflogEntry e : r.getReverseEntries()) {
- Instant timestamp = e.getWho().getWhen().toInstant();
+ Instant timestamp = e.getWho().getWhenAsInstant();
if ((from == null || from.isBefore(timestamp)) && (to == null || to.isAfter(timestamp))) {
entries.add(e);
}
diff --git a/java/com/google/gerrit/server/restapi/project/GetSubmitRequirement.java b/java/com/google/gerrit/server/restapi/project/GetSubmitRequirement.java
new file mode 100644
index 0000000..ce482e3
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/GetSubmitRequirement.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.project;
+
+import com.google.gerrit.extensions.common.SubmitRequirementInfo;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.project.SubmitRequirementJson;
+import com.google.gerrit.server.project.SubmitRequirementResource;
+import com.google.inject.Singleton;
+
+/** A rest read view that retrieves a "submit requirement" for a project by its name. */
+@Singleton
+public class GetSubmitRequirement implements RestReadView<SubmitRequirementResource> {
+ @Override
+ public Response<SubmitRequirementInfo> apply(SubmitRequirementResource rsrc) {
+ return Response.ok(SubmitRequirementJson.format(rsrc.getSubmitRequirement()));
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/ListTags.java b/java/com/google/gerrit/server/restapi/project/ListTags.java
index eccdcfc..ac0dff9 100644
--- a/java/com/google/gerrit/server/restapi/project/ListTags.java
+++ b/java/com/google/gerrit/server/restapi/project/ListTags.java
@@ -172,9 +172,6 @@
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 {
@@ -200,12 +197,12 @@
tagger != null ? CommonConverters.toGitPerson(tagger) : null,
canDelete,
webLinks.isEmpty() ? null : webLinks,
- tagger != null ? tagger.getWhen().toInstant() : null);
+ tagger != null ? tagger.getWhenAsInstant() : null);
}
Instant timestamp =
object instanceof RevCommit
- ? ((RevCommit) object).getCommitterIdent().getWhen().toInstant()
+ ? ((RevCommit) object).getCommitterIdent().getWhenAsInstant()
: null;
// Lightweight tag
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java b/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
index e50a494..1752b4ec 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
@@ -21,6 +21,7 @@
import static com.google.gerrit.server.project.FileResource.FILE_KIND;
import static com.google.gerrit.server.project.LabelResource.LABEL_KIND;
import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
+import static com.google.gerrit.server.project.SubmitRequirementResource.SUBMIT_REQUIREMENT_KIND;
import static com.google.gerrit.server.project.TagResource.TAG_KIND;
import com.google.gerrit.extensions.registration.DynamicMap;
@@ -46,6 +47,7 @@
DynamicMap.mapOf(binder(), COMMIT_KIND);
DynamicMap.mapOf(binder(), TAG_KIND);
DynamicMap.mapOf(binder(), LABEL_KIND);
+ DynamicMap.mapOf(binder(), SUBMIT_REQUIREMENT_KIND);
DynamicSet.bind(binder(), GerritConfigListener.class).to(SetParent.class);
DynamicSet.bind(binder(), ProjectCreationValidationListener.class)
@@ -78,6 +80,11 @@
delete(LABEL_KIND).to(DeleteLabel.class);
postOnCollection(LABEL_KIND).to(PostLabels.class);
+ child(PROJECT_KIND, "submit_requirements").to(SubmitRequirementsCollection.class);
+ create(SUBMIT_REQUIREMENT_KIND).to(CreateSubmitRequirement.class);
+ put(SUBMIT_REQUIREMENT_KIND).to(UpdateSubmitRequirement.class);
+ get(SUBMIT_REQUIREMENT_KIND).to(GetSubmitRequirement.class);
+
get(PROJECT_KIND, "HEAD").to(GetHead.class);
put(PROJECT_KIND, "HEAD").to(SetHead.class);
diff --git a/java/com/google/gerrit/server/restapi/project/SubmitRequirementsCollection.java b/java/com/google/gerrit/server/restapi/project/SubmitRequirementsCollection.java
new file mode 100644
index 0000000..cd98bec
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/SubmitRequirementsCollection.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.project;
+
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.SubmitRequirementResource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class SubmitRequirementsCollection
+ implements ChildCollection<ProjectResource, SubmitRequirementResource> {
+ private final Provider<CurrentUser> user;
+ private final PermissionBackend permissionBackend;
+ private final DynamicMap<RestView<SubmitRequirementResource>> views;
+
+ @Inject
+ SubmitRequirementsCollection(
+ Provider<CurrentUser> user,
+ PermissionBackend permissionBackend,
+ DynamicMap<RestView<SubmitRequirementResource>> views) {
+ this.user = user;
+ this.permissionBackend = permissionBackend;
+ this.views = views;
+ }
+
+ @Override
+ public RestView<ProjectResource> list() throws RestApiException {
+ /** TODO(ghareeb): implement. */
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public SubmitRequirementResource parse(ProjectResource parent, IdString id)
+ throws AuthException, ResourceNotFoundException, PermissionBackendException {
+ if (!user.get().isIdentifiedUser()) {
+ throw new AuthException("Authentication required");
+ }
+
+ permissionBackend
+ .currentUser()
+ .project(parent.getNameKey())
+ .check(ProjectPermission.READ_CONFIG);
+
+ SubmitRequirement submitRequirement =
+ parent.getProjectState().getConfig().getSubmitRequirementSections().get(id.get());
+
+ if (submitRequirement == null) {
+ throw new ResourceNotFoundException(
+ String.format("Submit requirement '%s' does not exist", id));
+ }
+ return new SubmitRequirementResource(parent, submitRequirement);
+ }
+
+ @Override
+ public DynamicMap<RestView<SubmitRequirementResource>> views() {
+ return views;
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/UpdateSubmitRequirement.java b/java/com/google/gerrit/server/restapi/project/UpdateSubmitRequirement.java
new file mode 100644
index 0000000..a176bc4
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/UpdateSubmitRequirement.java
@@ -0,0 +1,156 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.project;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementExpression;
+import com.google.gerrit.extensions.common.SubmitRequirementInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementInput;
+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.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.SubmitRequirementExpressionsValidator;
+import com.google.gerrit.server.project.SubmitRequirementJson;
+import com.google.gerrit.server.project.SubmitRequirementResource;
+import com.google.gerrit.server.project.SubmitRequirementsUtil;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/**
+ * A rest modify view that updates the definition of an existing submit requirement for a project.
+ */
+@Singleton
+public class UpdateSubmitRequirement
+ implements RestModifyView<SubmitRequirementResource, SubmitRequirementInput> {
+ private final Provider<CurrentUser> user;
+ private final PermissionBackend permissionBackend;
+ private final MetaDataUpdate.User updateFactory;
+ private final ProjectConfig.Factory projectConfigFactory;
+ private final ProjectCache projectCache;
+ private final SubmitRequirementExpressionsValidator submitRequirementExpressionsValidator;
+
+ @Inject
+ public UpdateSubmitRequirement(
+ Provider<CurrentUser> user,
+ PermissionBackend permissionBackend,
+ MetaDataUpdate.User updateFactory,
+ ProjectConfig.Factory projectConfigFactory,
+ ProjectCache projectCache,
+ SubmitRequirementExpressionsValidator submitRequirementExpressionsValidator) {
+ this.user = user;
+ this.permissionBackend = permissionBackend;
+ this.updateFactory = updateFactory;
+ this.projectConfigFactory = projectConfigFactory;
+ this.projectCache = projectCache;
+ this.submitRequirementExpressionsValidator = submitRequirementExpressionsValidator;
+ }
+
+ @Override
+ public Response<SubmitRequirementInfo> apply(
+ SubmitRequirementResource rsrc, SubmitRequirementInput input)
+ throws AuthException, BadRequestException, PermissionBackendException, IOException {
+ if (!user.get().isIdentifiedUser()) {
+ throw new AuthException("Authentication required");
+ }
+
+ permissionBackend
+ .currentUser()
+ .project(rsrc.getProject().getNameKey())
+ .check(ProjectPermission.WRITE_CONFIG);
+
+ if (input == null) {
+ input = new SubmitRequirementInput();
+ }
+
+ if (input.name != null && !input.name.equals(rsrc.getSubmitRequirement().name())) {
+ throw new BadRequestException("name in input must match name in URL");
+ }
+
+ try (MetaDataUpdate md = updateFactory.create(rsrc.getProject().getNameKey())) {
+ ProjectConfig config = projectConfigFactory.read(md);
+
+ SubmitRequirement submitRequirement =
+ createSubmitRequirement(config, rsrc.getSubmitRequirement().name(), input);
+
+ md.setMessage(String.format("Update Submit Requirement %s", submitRequirement.name()));
+ config.commit(md);
+
+ projectCache.evict(rsrc.getProject().getNameKey());
+
+ return Response.created(SubmitRequirementJson.format(submitRequirement));
+ } catch (ConfigInvalidException e) {
+ throw new IOException("Failed to read project config", e);
+ } catch (ResourceConflictException e) {
+ throw new BadRequestException("Failed to create submit requirement", e);
+ }
+ }
+
+ public SubmitRequirement createSubmitRequirement(
+ ProjectConfig config, String name, SubmitRequirementInput input)
+ throws BadRequestException, ResourceConflictException {
+ validateSRName(name);
+ if (Strings.isNullOrEmpty(input.submittabilityExpression)) {
+ throw new BadRequestException("submittability_expression is required");
+ }
+ if (input.allowOverrideInChildProjects == null) {
+ // default is false
+ input.allowOverrideInChildProjects = false;
+ }
+ SubmitRequirement submitRequirement =
+ SubmitRequirement.builder()
+ .setName(name)
+ .setDescription(Optional.ofNullable(input.description))
+ .setApplicabilityExpression(
+ SubmitRequirementExpression.of(input.applicabilityExpression))
+ .setSubmittabilityExpression(
+ SubmitRequirementExpression.create(input.submittabilityExpression))
+ .setOverrideExpression(SubmitRequirementExpression.of(input.overrideExpression))
+ .setAllowOverrideInChildProjects(input.allowOverrideInChildProjects)
+ .build();
+
+ List<String> validationMessages =
+ submitRequirementExpressionsValidator.validateExpressions(submitRequirement);
+ if (!validationMessages.isEmpty()) {
+ throw new BadRequestException(
+ String.format("Invalid submit requirement input: %s", validationMessages));
+ }
+
+ config.upsertSubmitRequirement(submitRequirement);
+ return submitRequirement;
+ }
+
+ private void validateSRName(String name) throws BadRequestException {
+ try {
+ SubmitRequirementsUtil.validateName(name);
+ } catch (IllegalArgumentException e) {
+ throw new BadRequestException(e.getMessage(), e);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 58db331..1a49171 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -16,7 +16,6 @@
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static java.util.Comparator.comparing;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toSet;
@@ -72,8 +71,6 @@
import com.google.gerrit.server.notedb.StoreSubmitRequirementsOp;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
@@ -247,7 +244,6 @@
private final RetryHelper retryHelper;
private final ChangeData.Factory changeDataFactory;
private final StoreSubmitRequirementsOp.Factory storeSubmitRequirementsOpFactory;
- private final ProjectCache projectCache;
// Changes that were updated by this MergeOp.
private final Map<Change.Id, Change> updatedChanges;
@@ -282,8 +278,7 @@
TopicMetrics topicMetrics,
RetryHelper retryHelper,
ChangeData.Factory changeDataFactory,
- StoreSubmitRequirementsOp.Factory storeSubmitRequirementsOpFactory,
- ProjectCache projectCache) {
+ StoreSubmitRequirementsOp.Factory storeSubmitRequirementsOpFactory) {
this.cmUtil = cmUtil;
this.batchUpdateFactory = batchUpdateFactory;
this.internalUserFactory = internalUserFactory;
@@ -301,7 +296,6 @@
this.changeDataFactory = changeDataFactory;
this.updatedChanges = new HashMap<>();
this.storeSubmitRequirementsOpFactory = storeSubmitRequirementsOpFactory;
- this.projectCache = projectCache;
}
@Override
@@ -654,12 +648,12 @@
Project.NameKey project = entry.getValue().project();
Change.Id changeId = entry.getKey();
ChangeData cd = entry.getValue();
- Collection<SubmitRequirementResult> srResults =
- cd.submitRequirementsIncludingLegacy().values();
batchUpdatesByProject
.get(project)
- .addOp(changeId, storeSubmitRequirementsOpFactory.create(srResults, cd));
- crossCheckSubmitRequirementResults(cd, srResults, project);
+ .addOp(
+ changeId,
+ storeSubmitRequirementsOpFactory.create(
+ cd.submitRequirementsIncludingLegacy().values(), cd));
}
try {
submissionExecutor.setAdditionalBatchUpdateListeners(
@@ -1009,28 +1003,4 @@
+ " projects involved; some projects may have submitted successfully, but others may have"
+ " failed";
}
-
- /**
- * Make sure that for every project config submit requirement there exists a corresponding result
- * with the same name in {@code srResults}. If no result is found, log a warning message.
- */
- private void crossCheckSubmitRequirementResults(
- ChangeData cd, Collection<SubmitRequirementResult> srResults, Project.NameKey project) {
- ProjectState state = projectCache.get(project).orElseThrow(illegalState(project));
- Map<String, SubmitRequirement> projectConfigRequirements = state.getSubmitRequirements();
- for (String srName : projectConfigRequirements.keySet()) {
- boolean hasResult = false;
- for (SubmitRequirementResult srResult : srResults) {
- if (!srResult.isLegacy() && srResult.submitRequirement().name().equals(srName)) {
- hasResult = true;
- break;
- }
- }
- if (!hasResult) {
- logger.atWarning().log(
- "Change %d: No result found for project config submit requirement '%s'",
- cd.getId().get(), srName);
- }
- }
- }
}
diff --git a/java/com/google/gerrit/server/update/BatchUpdate.java b/java/com/google/gerrit/server/update/BatchUpdate.java
index b247552..3dfd9b4 100644
--- a/java/com/google/gerrit/server/update/BatchUpdate.java
+++ b/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -69,6 +69,7 @@
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.time.Instant;
+import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -76,7 +77,6 @@
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
-import java.util.TimeZone;
import java.util.TreeMap;
import java.util.function.Function;
import org.eclipse.jgit.lib.BatchRefUpdate;
@@ -257,8 +257,8 @@
}
@Override
- public TimeZone getTimeZone() {
- return tz;
+ public ZoneId getZoneId() {
+ return zoneId;
}
@Override
@@ -383,7 +383,7 @@
private final Project.NameKey project;
private final CurrentUser user;
private final Instant when;
- private final TimeZone tz;
+ private final ZoneId zoneId;
private final ListMultimap<Change.Id, BatchUpdateOp> ops =
MultimapBuilder.linkedHashKeys().arrayListValues().build();
@@ -422,7 +422,7 @@
this.project = project;
this.user = user;
this.when = when;
- tz = serverIdent.getTimeZone();
+ zoneId = serverIdent.getZoneId();
}
@Override
@@ -666,7 +666,7 @@
repo, repoView.getRevWalk(), repoView.getInserter(), repoView.getCommands()),
dryrun);
if (user.isIdentifiedUser()) {
- handle.manager.setRefLogIdent(user.asIdentifiedUser().newRefLogIdent(when, tz));
+ handle.manager.setRefLogIdent(user.asIdentifiedUser().newRefLogIdent(when, zoneId));
}
handle.manager.setRefLogMessage(refLogMessage);
handle.manager.setPushCertificate(pushCert);
diff --git a/java/com/google/gerrit/server/update/ChainedReceiveCommands.java b/java/com/google/gerrit/server/update/ChainedReceiveCommands.java
index 99c72f2..5ff8d33 100644
--- a/java/com/google/gerrit/server/update/ChainedReceiveCommands.java
+++ b/java/com/google/gerrit/server/update/ChainedReceiveCommands.java
@@ -39,13 +39,19 @@
public class ChainedReceiveCommands implements RefCache {
private final Map<String, ReceiveCommand> commands = new LinkedHashMap<>();
private final RepoRefCache refCache;
+ private final boolean closeRefCache;
public ChainedReceiveCommands(Repository repo) {
- this(new RepoRefCache(repo));
+ this(new RepoRefCache(repo), true);
}
public ChainedReceiveCommands(RepoRefCache refCache) {
+ this(refCache, false);
+ }
+
+ private ChainedReceiveCommands(RepoRefCache refCache, boolean closeRefCache) {
this.refCache = requireNonNull(refCache);
+ this.closeRefCache = closeRefCache;
}
public RepoRefCache getRepoRefCache() {
@@ -122,4 +128,11 @@
public Map<String, ReceiveCommand> getCommands() {
return Collections.unmodifiableMap(commands);
}
+
+ @Override
+ public void close() {
+ if (closeRefCache) {
+ refCache.close();
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/update/Context.java b/java/com/google/gerrit/server/update/Context.java
index 57ebedd..aa41d90 100644
--- a/java/com/google/gerrit/server/update/Context.java
+++ b/java/com/google/gerrit/server/update/Context.java
@@ -25,7 +25,7 @@
import com.google.gerrit.server.change.NotifyResolver;
import java.io.IOException;
import java.time.Instant;
-import java.util.TimeZone;
+import java.time.ZoneId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -70,13 +70,13 @@
Instant getWhen();
/**
- * Get the time zone in which this update takes place.
+ * Get the time zone ID in which this update takes place.
*
- * <p>In the current implementation, this is always the time zone of the server.
+ * <p>In the current implementation, this is always the time zone ID of the server.
*
- * @return time zone.
+ * @return zone ID.
*/
- TimeZone getTimeZone();
+ ZoneId getZoneId();
/**
* Get the user performing the update.
@@ -162,6 +162,6 @@
* @return the created committer {@link PersonIdent}
*/
default PersonIdent newCommitterIdent(IdentifiedUser user) {
- return user.newCommitterIdent(getWhen(), getTimeZone());
+ return user.newCommitterIdent(getWhen(), getZoneId());
}
}
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index b9daa13..86ceb60 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -37,6 +37,7 @@
import com.google.gerrit.server.FanOutExecutor;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.GerritPersonIdentProvider;
+import com.google.gerrit.server.LibModuleType;
import com.google.gerrit.server.PluginUser;
import com.google.gerrit.server.api.GerritApiModule;
import com.google.gerrit.server.api.PluginApiModule;
@@ -243,13 +244,19 @@
bind(AllChangesIndexer.class).toProvider(Providers.of(null));
bind(AllGroupsIndexer.class).toProvider(Providers.of(null));
- String indexTypeCfg = cfg.getString("index", null, "type");
- IndexType indexType = new IndexType(indexTypeCfg != null ? indexTypeCfg : "fake");
- // For custom index types, callers must provide their own module.
- if (indexType.isLucene()) {
- install(luceneIndexModule());
- } else if (indexType.isFake()) {
- install(fakeIndexModule());
+ // Index lib module has a higher priority than index type configuration.
+ String indexModule =
+ cfg.getString("index", null, "install" + LibModuleType.INDEX_MODULE_TYPE.getConfigKey());
+ if (indexModule != null) {
+ install(indexModule(indexModule));
+ } else {
+ String indexTypeCfg = cfg.getString("index", null, "type");
+ IndexType indexType = new IndexType(indexTypeCfg != null ? indexTypeCfg : "fake");
+ if (indexType.isLucene()) {
+ install(luceneIndexModule());
+ } else if (indexType.isFake()) {
+ install(fakeIndexModule());
+ }
}
bind(ServerInformationImpl.class);
bind(ServerInformation.class).to(ServerInformationImpl.class);
diff --git a/java/com/google/gerrit/testing/TestChanges.java b/java/com/google/gerrit/testing/TestChanges.java
index 8bd02b8..4a97bc5 100644
--- a/java/com/google/gerrit/testing/TestChanges.java
+++ b/java/com/google/gerrit/testing/TestChanges.java
@@ -31,7 +31,7 @@
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.Injector;
-import java.util.TimeZone;
+import java.time.ZoneId;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
@@ -109,7 +109,7 @@
try (Repository repo = repoManager.openRepository(c.getProject());
TestRepository<Repository> tr = new TestRepository<>(repo)) {
PersonIdent ident =
- user.asIdentifiedUser().newCommitterIdent(update.getWhen(), TimeZone.getDefault());
+ user.asIdentifiedUser().newCommitterIdent(update.getWhen(), ZoneId.systemDefault());
TestRepository<Repository>.CommitBuilder cb =
tr.commit()
.author(ident)
diff --git a/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java b/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
index 877ccd5..b6e5b74 100644
--- a/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
+++ b/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
@@ -36,7 +36,6 @@
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;
@@ -333,13 +332,10 @@
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"), Date.from(TimeUtil.now()));
+ new PersonIdent(new PersonIdent("Foo Bar", "foo.bar@baz.com"), 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/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 78a0eeb..9f47925 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -158,7 +158,6 @@
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;
@@ -2466,9 +2465,6 @@
}
@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();
@@ -2482,7 +2478,7 @@
RevWalk rw = new RevWalk(repo)) {
RevCommit commit = rw.parseCommit(repo.exactRef(userRef).getObjectId());
- PersonIdent ident = new PersonIdent(serverIdent.get(), Date.from(TimeUtil.now()));
+ PersonIdent ident = new PersonIdent(serverIdent.get(), 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 c5f0d23..e8ef187 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -83,6 +83,7 @@
import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.acceptance.UseTimezone;
import com.google.gerrit.acceptance.VerifyNoPiiInChangeNotes;
+import com.google.gerrit.acceptance.api.change.ChangeIT.TestAttentionSetListenerModule.TestAttentionSetListener;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.acceptance.testsuite.change.IndexOperations;
@@ -147,7 +148,9 @@
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.common.TrackingIdInfo;
+import com.google.gerrit.extensions.events.AttentionSetListener;
import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
@@ -1536,6 +1539,39 @@
}
@Test
+ public void attentionSetListener_firesOnChange() throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ AttentionSetInput addUser = new AttentionSetInput(user.email(), "some reason");
+ TestAttentionSetListener attentionSetListener = new TestAttentionSetListener();
+
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(attentionSetListener)) {
+
+ gApi.changes().id(r1.getChangeId()).addReviewer(user.email());
+ gApi.changes().id(r1.getChangeId()).addToAttentionSet(addUser);
+
+ assertThat(attentionSetListener.fired).isTrue();
+ assertThat(attentionSetListener.lastEvent.usersAdded().size()).isEqualTo(1);
+ attentionSetListener
+ .lastEvent
+ .usersAdded()
+ .forEach(u -> assertThat(u).isEqualTo(user.id().get()));
+ assertThat(attentionSetListener.lastEvent.usersRemoved()).isEmpty();
+
+ attentionSetListener.fired = false;
+ gApi.changes().id(r1.getChangeId()).attention(user.username()).remove(addUser);
+
+ assertThat(attentionSetListener.fired).isTrue();
+ assertThat(attentionSetListener.lastEvent.usersAdded()).isEmpty();
+ assertThat(attentionSetListener.lastEvent.usersRemoved().size()).isEqualTo(1);
+ attentionSetListener
+ .lastEvent
+ .usersRemoved()
+ .forEach(u -> assertThat(u).isEqualTo(user.id().get()));
+ }
+ }
+
+ @Test
public void rebaseChangeBase() throws Exception {
PushOneCommit.Result r1 = createChange();
PushOneCommit.Result r2 = createChange();
@@ -4752,6 +4788,27 @@
}
}
+ public static class TestAttentionSetListenerModule extends AbstractModule {
+ @Override
+ public void configure() {
+ DynamicSet.bind(binder(), AttentionSetListener.class).to(TestAttentionSetListener.class);
+ }
+
+ public static class TestAttentionSetListener implements AttentionSetListener {
+ Event lastEvent;
+ boolean fired;
+
+ @Inject
+ public TestAttentionSetListener() {}
+
+ @Override
+ public void onAttentionSetChanged(Event event) {
+ fired = true;
+ lastEvent = event;
+ }
+ }
+ }
+
private void voteLabel(String changeId, String labelName, int score) throws RestApiException {
gApi.changes().id(changeId).current().review(new ReviewInput().label(labelName, score));
}
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 63b67f8..d630296 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -112,7 +112,6 @@
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;
@@ -1608,9 +1607,6 @@
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()) {
@@ -1618,7 +1614,7 @@
treeId = oi.insert(Constants.OBJ_TREE, new byte[] {});
}
- PersonIdent ident = new PersonIdent(serverIdent.get(), Date.from(TimeUtil.now()));
+ PersonIdent ident = new PersonIdent(serverIdent.get(), TimeUtil.now());
CommitBuilder cb = new CommitBuilder();
cb.setTreeId(treeId);
cb.setCommitter(ident);
diff --git a/javatests/com/google/gerrit/acceptance/api/project/SubmitRequirementsAPIIT.java b/javatests/com/google/gerrit/acceptance/api/project/SubmitRequirementsAPIIT.java
new file mode 100644
index 0000000..b8f4f42
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/project/SubmitRequirementsAPIIT.java
@@ -0,0 +1,471 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.extensions.common.SubmitRequirementInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+@NoHttpd
+public class SubmitRequirementsAPIIT extends AbstractDaemonTest {
+ @Inject private RequestScopeOperations requestScopeOperations;
+
+ @Test
+ public void cannotGetANonExistingSR() throws Exception {
+ ResourceNotFoundException thrown =
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> gApi.projects().name(project.get()).submitRequirement("code-review").get());
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo("Submit requirement 'code-review' does not exist");
+ }
+
+ @Test
+ public void getExistingSR() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.applicabilityExpression = "topic:foo";
+ input.submittabilityExpression = "label:code-review=+2";
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+
+ SubmitRequirementInfo info =
+ gApi.projects().name(project.get()).submitRequirement("code-review").get();
+ assertThat(info.name).isEqualTo("code-review");
+ assertThat(info.applicabilityExpression).isEqualTo("topic:foo");
+ assertThat(info.submittabilityExpression).isEqualTo("label:code-review=+2");
+ assertThat(info.allowOverrideInChildProjects).isEqualTo(false);
+ }
+
+ @Test
+ public void updateSubmitRequirement() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.applicabilityExpression = "topic:foo";
+ input.submittabilityExpression = "label:code-review=+2";
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+
+ input.submittabilityExpression = "label:code-review=+1";
+ SubmitRequirementInfo info =
+ gApi.projects().name(project.get()).submitRequirement("code-review").update(input);
+ assertThat(info.submittabilityExpression).isEqualTo("label:code-review=+1");
+ }
+
+ @Test
+ public void updateSRWithEmptyApplicabilityExpression_isAllowed() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.applicabilityExpression = "topic:foo";
+ input.submittabilityExpression = "label:code-review=+2";
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+
+ input.applicabilityExpression = null;
+ SubmitRequirementInfo info =
+ gApi.projects().name(project.get()).submitRequirement("code-review").update(input);
+ assertThat(info.applicabilityExpression).isNull();
+ }
+
+ @Test
+ public void updateSRWithEmptyOverrideExpression_isAllowed() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.overrideExpression = "topic:foo";
+ input.submittabilityExpression = "label:code-review=+2";
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+
+ input.overrideExpression = null;
+ SubmitRequirementInfo info =
+ gApi.projects().name(project.get()).submitRequirement("code-review").update(input);
+ assertThat(info.overrideExpression).isNull();
+ }
+
+ @Test
+ public void allowOverrideInChildProjectsDefaultsToFalse_updateSR() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.submittabilityExpression = "label:code-review=+2";
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+
+ input.overrideExpression = "topic:foo";
+ SubmitRequirementInfo info =
+ gApi.projects().name(project.get()).submitRequirement("code-review").update(input);
+ assertThat(info.allowOverrideInChildProjects).isFalse();
+ }
+
+ @Test
+ public void cannotUpdateSRAsAnonymousUser() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.submittabilityExpression = "label:code-review=+2";
+
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+ input.submittabilityExpression = "label:code-review=+1";
+ requestScopeOperations.setApiUserAnonymous();
+ AuthException thrown =
+ assertThrows(
+ AuthException.class,
+ () ->
+ gApi.projects()
+ .name(project.get())
+ .submitRequirement("code-review")
+ .update(new SubmitRequirementInput()));
+ assertThat(thrown).hasMessageThat().contains("Authentication required");
+ }
+
+ @Test
+ public void cannotUpdateSRtIfSRDoesNotExist() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.description = "At least one +2 vote to the code-review label";
+ input.submittabilityExpression = "label:code-review=+2";
+
+ ResourceNotFoundException thrown =
+ assertThrows(
+ ResourceNotFoundException.class,
+ () ->
+ gApi.projects().name(project.get()).submitRequirement("code-review").update(input));
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo("Submit requirement 'code-review' does not exist");
+ }
+
+ @Test
+ public void cannotUpdateSRWithEmptySubmittableIf() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.submittabilityExpression = "project:foo AND branch:refs/heads/main";
+
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+ input.submittabilityExpression = null;
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () ->
+ gApi.projects().name(project.get()).submitRequirement("code-review").update(input));
+
+ assertThat(thrown).hasMessageThat().isEqualTo("submittability_expression is required");
+ }
+
+ @Test
+ public void cannotUpdateSRWithInvalidSubmittableIfExpression() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.submittabilityExpression = "project:foo AND branch:refs/heads/main";
+
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+ input.submittabilityExpression = "invalid_field:invalid_value";
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () ->
+ gApi.projects().name(project.get()).submitRequirement("code-review").update(input));
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo(
+ "Invalid submit requirement input: "
+ + "[Invalid project configuration, "
+ + "project.config: Expression 'invalid_field:invalid_value' of "
+ + "submit requirement 'code-review' "
+ + "(parameter submit-requirement.code-review.submittableIf) is invalid: "
+ + "Unsupported operator invalid_field:invalid_value]");
+ }
+
+ @Test
+ public void cannotUpdateSRWithInvalidOverrideIfExpression() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.submittabilityExpression = "project:foo AND branch:refs/heads/main";
+
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+ input.overrideExpression = "invalid_field:invalid_value";
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () ->
+ gApi.projects().name(project.get()).submitRequirement("code-review").update(input));
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo(
+ "Invalid submit requirement input: "
+ + "[Invalid project configuration, "
+ + "project.config: Expression 'invalid_field:invalid_value' of "
+ + "submit requirement 'code-review' "
+ + "(parameter submit-requirement.code-review.overrideIf) is invalid: "
+ + "Unsupported operator invalid_field:invalid_value]");
+ }
+
+ @Test
+ public void cannotUpdateSRWithInvalidApplicableIfExpression() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.submittabilityExpression = "project:foo AND branch:refs/heads/main";
+
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+ input.applicabilityExpression = "invalid_field:invalid_value";
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () ->
+ gApi.projects().name(project.get()).submitRequirement("code-review").update(input));
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo(
+ "Invalid submit requirement input: "
+ + "[Invalid project configuration, "
+ + "project.config: Expression 'invalid_field:invalid_value' of "
+ + "submit requirement 'code-review' "
+ + "(parameter submit-requirement.code-review.applicableIf) is invalid: "
+ + "Unsupported operator invalid_field:invalid_value]");
+ }
+
+ @Test
+ public void createSubmitRequirement() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.description = "At least one +2 vote to the code-review label";
+ input.applicabilityExpression = "project:foo AND branch:refs/heads/main";
+ input.submittabilityExpression = "label:code-review=+2";
+ input.overrideExpression = "label:build-cop-override=+1";
+ input.allowOverrideInChildProjects = true;
+
+ SubmitRequirementInfo info =
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+
+ assertThat(info.name).isEqualTo("code-review");
+ assertThat(info.description).isEqualTo(input.description);
+ assertThat(info.applicabilityExpression).isEqualTo(input.applicabilityExpression);
+ assertThat(info.applicabilityExpression).isEqualTo(input.applicabilityExpression);
+ assertThat(info.submittabilityExpression).isEqualTo(input.submittabilityExpression);
+ assertThat(info.overrideExpression).isEqualTo(input.overrideExpression);
+ assertThat(info.allowOverrideInChildProjects).isEqualTo(true);
+ }
+
+ @Test
+ public void createSRWithEmptyApplicabilityExpression_isAllowed() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.description = "At least one +2 vote to the code-review label";
+ input.submittabilityExpression = "label:code-review=+2";
+ input.overrideExpression = "label:build-cop-override=+1";
+
+ SubmitRequirementInfo info =
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+
+ assertThat(info.name).isEqualTo("code-review");
+ assertThat(info.applicabilityExpression).isNull();
+ }
+
+ @Test
+ public void createSRWithEmptyOverrideExpression_isAllowed() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.description = "At least one +2 vote to the code-review label";
+ input.applicabilityExpression = "project:foo AND branch:refs/heads/main";
+ input.submittabilityExpression = "label:code-review=+2";
+
+ SubmitRequirementInfo info =
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+
+ assertThat(info.name).isEqualTo("code-review");
+ assertThat(info.overrideExpression).isNull();
+ }
+
+ @Test
+ public void allowOverrideInChildProjectsDefaultsToFalse_createSR() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.description = "At least one +2 vote to the code-review label";
+ input.submittabilityExpression = "label:code-review=+2";
+
+ SubmitRequirementInfo info =
+ gApi.projects().name(project.get()).submitRequirement("code-review").create(input).get();
+
+ assertThat(info.allowOverrideInChildProjects).isEqualTo(false);
+ }
+
+ @Test
+ public void cannotCreateSRAsAnonymousUser() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.description = "At least one +2 vote to the code-review label";
+ input.applicabilityExpression = "project:foo AND branch:refs/heads/main";
+ input.submittabilityExpression = "label:code-review=+2";
+ input.overrideExpression = "label:build-cop-override=+1";
+
+ requestScopeOperations.setApiUserAnonymous();
+ AuthException thrown =
+ assertThrows(
+ AuthException.class,
+ () ->
+ gApi.projects()
+ .name(project.get())
+ .submitRequirement("code-review")
+ .create(new SubmitRequirementInput()));
+ assertThat(thrown).hasMessageThat().contains("Authentication required");
+ }
+
+ @Test
+ public void cannotCreateSRtIfNameInInputDoesNotMatchResource() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.description = "At least one +2 vote to the code-review label";
+ input.submittabilityExpression = "label:code-review=+2";
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () ->
+ gApi.projects()
+ .name(project.get())
+ .submitRequirement("other-requirement")
+ .create(input)
+ .get());
+ assertThat(thrown).hasMessageThat().isEqualTo("name in input must match name in URL");
+ }
+
+ @Test
+ public void cannotCreateSRWithInvalidName() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "wrong$%";
+ input.submittabilityExpression = "label:code-review=+2";
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () ->
+ gApi.projects()
+ .name(project.get())
+ .submitRequirement("wrong$%")
+ .create(input)
+ .get());
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo(
+ "Illegal submit requirement name \"wrong$%\". "
+ + "Name can only consist of alphanumeric characters and '-'."
+ + " Name cannot start with '-' or number.");
+ }
+
+ @Test
+ public void cannotCreateSRWithEmptySubmittableIf() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.description = "At least one +2 vote to the code-review label";
+ input.applicabilityExpression = "project:foo AND branch:refs/heads/main";
+ input.overrideExpression = "label:build-cop-override=+1";
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () ->
+ gApi.projects()
+ .name(project.get())
+ .submitRequirement("code-review")
+ .create(input)
+ .get());
+
+ assertThat(thrown).hasMessageThat().isEqualTo("submittability_expression is required");
+ }
+
+ @Test
+ public void cannotCreateSRWithInvalidSubmittableIfExpression() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.description = "At least one +2 vote to the code-review label";
+ input.submittabilityExpression = "invalid_field:invalid_value";
+ BadRequestException exception =
+ assertThrows(
+ BadRequestException.class,
+ () ->
+ gApi.projects()
+ .name(project.get())
+ .submitRequirement("code-review")
+ .create(input)
+ .get());
+ assertThat(exception)
+ .hasMessageThat()
+ .isEqualTo(
+ "Invalid submit requirement input: "
+ + "[Invalid project configuration, "
+ + "project.config: Expression 'invalid_field:invalid_value' of "
+ + "submit requirement 'code-review' "
+ + "(parameter submit-requirement.code-review.submittableIf) is invalid: "
+ + "Unsupported operator invalid_field:invalid_value]");
+ }
+
+ @Test
+ public void cannotCreateSRWithInvalidOverrideIfExpression() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.description = "At least one +2 vote to the code-review label";
+ input.submittabilityExpression = "label:Code-Review=+2";
+ input.overrideExpression = "invalid_field:invalid_value";
+ BadRequestException exception =
+ assertThrows(
+ BadRequestException.class,
+ () ->
+ gApi.projects()
+ .name(project.get())
+ .submitRequirement("code-review")
+ .create(input)
+ .get());
+ assertThat(exception)
+ .hasMessageThat()
+ .isEqualTo(
+ "Invalid submit requirement input: "
+ + "[Invalid project configuration, "
+ + "project.config: Expression 'invalid_field:invalid_value' of "
+ + "submit requirement 'code-review' "
+ + "(parameter submit-requirement.code-review.overrideIf) is invalid: "
+ + "Unsupported operator invalid_field:invalid_value]");
+ }
+
+ @Test
+ public void cannotCreateSRWithInvalidApplicableIfExpression() throws Exception {
+ SubmitRequirementInput input = new SubmitRequirementInput();
+ input.name = "code-review";
+ input.description = "At least one +2 vote to the code-review label";
+ input.applicabilityExpression = "invalid_field:invalid_value";
+ input.submittabilityExpression = "label:Code-Review=+2";
+ BadRequestException exception =
+ assertThrows(
+ BadRequestException.class,
+ () ->
+ gApi.projects()
+ .name(project.get())
+ .submitRequirement("code-review")
+ .create(input)
+ .get());
+ assertThat(exception)
+ .hasMessageThat()
+ .isEqualTo(
+ "Invalid submit requirement input: "
+ + "[Invalid project configuration, "
+ + "project.config: Expression 'invalid_field:invalid_value' of "
+ + "submit requirement 'code-review' "
+ + "(parameter submit-requirement.code-review.applicableIf) is invalid: "
+ + "Unsupported operator invalid_field:invalid_value]");
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index efd3cea..1919810 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -3005,9 +3005,6 @@
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 =
@@ -3030,14 +3027,14 @@
DateTimeFormatter fmt =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z")
.withLocale(Locale.US)
- .withZone(author.getTimeZone().toZoneId());
+ .withZone(author.getZoneId());
headers.add("Author: " + author.getName() + " <" + author.getEmailAddress() + ">");
- headers.add("AuthorDate: " + fmt.format(author.getWhen().toInstant()));
+ headers.add("AuthorDate: " + fmt.format(author.getWhenAsInstant()));
PersonIdent committer = c.getCommitterIdent();
- fmt = fmt.withZone(committer.getTimeZone().toZoneId());
+ fmt = fmt.withZone(committer.getZoneId());
headers.add("Commit: " + committer.getName() + " <" + committer.getEmailAddress() + ">");
- headers.add("CommitDate: " + fmt.format(committer.getWhen().toInstant()));
+ headers.add("CommitDate: " + fmt.format(committer.getWhenAsInstant()));
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 4a7849f..2c80333 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -1706,15 +1706,13 @@
}
}
- // 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.getTime()).isEqualTo(expectedIdent.getWhen().getTime());
+ assertThat(gitPerson.date.getTime()).isEqualTo(expectedIdent.getWhenAsInstant().toEpochMilli());
assertThat(gitPerson.tz).isEqualTo(expectedIdent.getTimeZoneOffset());
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
index f1c0110..8879d53 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
@@ -32,6 +32,8 @@
import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.SubmitRequirement;
+import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.api.projects.TagInput;
@@ -85,7 +87,8 @@
.build(),
RestCall.get("/projects/%s/dashboards"),
RestCall.put("/projects/%s/labels/new-label"),
- RestCall.post("/projects/%s/labels/"));
+ RestCall.post("/projects/%s/labels/"),
+ RestCall.put("/projects/%s/submit_requirements/new-sr"));
/**
* Child project REST endpoints to be tested, each URL contains placeholders for the parent
@@ -175,6 +178,15 @@
// Label deletion must be tested last
RestCall.delete("/projects/%s/labels/%s"));
+ /**
+ * Submit requirement REST endpoints to be tested, each URL contains placeholders for the project
+ * identifier and the submit requirement name.
+ */
+ private static final ImmutableList<RestCall> SUBMIT_REQUIREMENT_ENDPOINTS =
+ ImmutableList.of(
+ RestCall.get("/projects/%s/submit_requirements/%s"),
+ RestCall.put("/projects/%s/submit_requirements/%s"));
+
private static final String FILENAME = "test.txt";
@Inject private ProjectOperations projectOperations;
@@ -236,6 +248,20 @@
RestApiCallHelper.execute(adminRestSession, LABEL_ENDPOINTS, project.get(), label);
}
+ @Test
+ public void submitRequirementsEndpoints() throws Exception {
+ // Create the SR, so that the GET endpoint succeeds
+ configSubmitRequirement(
+ project,
+ SubmitRequirement.builder()
+ .setName("code-review")
+ .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
+ .setAllowOverrideInChildProjects(false)
+ .build());
+ RestApiCallHelper.execute(
+ adminRestSession, SUBMIT_REQUIREMENT_ENDPOINTS, project.get(), "code-review");
+ }
+
private String createAndSubmitChange(String filename) throws Exception {
RevCommit c =
testRepo
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 8eada79..0e4f212 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -1361,14 +1361,11 @@
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().getTime())
- .isEqualTo(commit.getCommitterIdent().getWhen().getTime());
- assertThat(commit.getAuthorIdent().getTimeZone())
- .isEqualTo(commit.getCommitterIdent().getTimeZone());
+ assertThat(commit.getAuthorIdent().getWhenAsInstant())
+ .isEqualTo(commit.getCommitterIdent().getWhenAsInstant());
+ assertThat(commit.getAuthorIdent().getZoneId())
+ .isEqualTo(commit.getCommitterIdent().getZoneId());
}
protected void assertSubmitter(String changeId, int psId) throws Throwable {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
index 044da19..c8c00a1 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -457,11 +457,8 @@
return gApi.projects().name(project.get()).tag(tagname);
}
- // 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();
+ return r.getCommit().getCommitterIdent().getWhenAsInstant();
}
private void assertBadRequest(ListRefsRequest<TagInfo> req) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index 5b6da36..bcde618 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -825,14 +825,11 @@
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().toInstant(), committer);
+ getAccount(admin.id()).id(), committer.getWhenAsInstant(), committer);
serverSideTestRepo
.branch(RefNames.changeMetaRef(id))
.commit()
diff --git a/javatests/com/google/gerrit/acceptance/server/change/DeleteZombieDraftIT.java b/javatests/com/google/gerrit/acceptance/server/change/DeleteZombieDraftIT.java
new file mode 100644
index 0000000..3d83175
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/change/DeleteZombieDraftIT.java
@@ -0,0 +1,95 @@
+// 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.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
+import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.server.notedb.DeleteZombieCommentsRefs;
+import com.google.inject.Inject;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Test;
+
+/** Test for {@link com.google.gerrit.server.notedb.DeleteZombieCommentsRefs}. */
+public class DeleteZombieDraftIT extends AbstractDaemonTest {
+
+ @Inject private DeleteZombieCommentsRefs.Factory deleteZombieDraftsFactory;
+
+ @Test
+ public void detectZombieDrafts() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ String revId = r.getCommit().getName();
+
+ DraftInput comment = CommentsUtil.newDraft("f1.txt", Side.REVISION, /* line= */ 1, "comment 1");
+ addDraft(changeId, revId, comment);
+ Ref draftRef = getOnlyDraftRef();
+ publishComments(r);
+ Map<String, List<CommentInfo>> drafts = getDraftComments(changeId, revId);
+ List<CommentInfo> publishedComments = getPublishedCommentsAsList(changeId);
+ assertThat(drafts).isEmpty();
+ assertThat(publishedComments).hasSize(1);
+
+ // Restore the draft ref, resulting in the comment existing twice as {draft, published}.
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers)) {
+ RefUpdate u = allUsersRepo.updateRef(draftRef.getName());
+ u.setNewObjectId(draftRef.getObjectId());
+ u.forceUpdate();
+ }
+
+ DeleteZombieCommentsRefs worker =
+ deleteZombieDraftsFactory.create(/* cleanupPercentage= */ 100);
+ assertThat(worker.getNumberOfDraftsThatAreAlsoPublished()).isEqualTo(1);
+ }
+
+ private Ref getOnlyDraftRef() throws Exception {
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers)) {
+ return Iterables.getOnlyElement(
+ allUsersRepo.getRefDatabase().getRefsByPrefix(RefNames.REFS_DRAFT_COMMENTS));
+ }
+ }
+
+ private void publishComments(PushOneCommit.Result r) throws Exception {
+ ReviewInput reviewInput = new ReviewInput();
+ reviewInput.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
+ reviewInput.message = "foo";
+ revision(r).review(reviewInput);
+ }
+
+ private Map<String, List<CommentInfo>> getDraftComments(String changeId, String revId)
+ throws Exception {
+ return gApi.changes().id(changeId).revision(revId).drafts();
+ }
+
+ private List<CommentInfo> getPublishedCommentsAsList(String changeId) throws Exception {
+ return gApi.changes().id(changeId).commentsRequest().getAsList();
+ }
+
+ private void addDraft(String changeId, String revId, DraftInput in) throws Exception {
+ gApi.changes().id(changeId).revision(revId).createDraft(in).get();
+ }
+}
diff --git a/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java b/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
index 1e3063e..5f062be 100644
--- a/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
+++ b/javatests/com/google/gerrit/server/account/UniversalGroupBackendTest.java
@@ -30,6 +30,7 @@
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.AccountGroup.UUID;
import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.metrics.DisabledMetricMaker;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.plugincontext.PluginContext.PluginMetrics;
@@ -56,7 +57,8 @@
backends.add("gerrit", new SystemGroupBackend(new Config()));
backend =
new UniversalGroupBackend(
- new PluginSetContext<>(backends, PluginMetrics.DISABLED_INSTANCE));
+ new PluginSetContext<>(backends, PluginMetrics.DISABLED_INSTANCE),
+ new DisabledMetricMaker());
}
@Test
@@ -124,7 +126,8 @@
backends.add("gerrit", backend);
backend =
new UniversalGroupBackend(
- new PluginSetContext<>(backends, PluginMetrics.DISABLED_INSTANCE));
+ new PluginSetContext<>(backends, PluginMetrics.DISABLED_INSTANCE),
+ new DisabledMetricMaker());
GroupMembership checker = backend.membershipsOf(member);
assertFalse(checker.contains(REGISTERED_USERS));
diff --git a/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java b/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
index c04deb4..1bb9784 100644
--- a/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
+++ b/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
@@ -18,10 +18,7 @@
import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
import java.util.function.Supplier;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletRequestWrapper;
import org.junit.Test;
public class PerThreadCacheTest {
@@ -47,7 +44,7 @@
@Test
public void endToEndCache() {
- try (PerThreadCache ignored = PerThreadCache.create(null)) {
+ try (PerThreadCache ignored = PerThreadCache.create()) {
PerThreadCache cache = PerThreadCache.get();
PerThreadCache.Key<String> key1 = PerThreadCache.Key.create(String.class);
@@ -65,7 +62,7 @@
@Test
public void cleanUp() {
PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class);
- try (PerThreadCache ignored = PerThreadCache.create(null)) {
+ try (PerThreadCache ignored = PerThreadCache.create()) {
PerThreadCache cache = PerThreadCache.get();
String value1 = cache.get(key, () -> "value1");
assertThat(value1).isEqualTo("value1");
@@ -73,7 +70,7 @@
// Create a second cache and assert that it is not connected to the first one.
// This ensures that the cleanup is actually working.
- try (PerThreadCache ignored = PerThreadCache.create(null)) {
+ try (PerThreadCache ignored = PerThreadCache.create()) {
PerThreadCache cache = PerThreadCache.get();
String value1 = cache.get(key, () -> "value2");
assertThat(value1).isEqualTo("value2");
@@ -82,48 +79,16 @@
@Test
public void doubleInstantiationFails() {
- try (PerThreadCache ignored = PerThreadCache.create(null)) {
+ try (PerThreadCache ignored = PerThreadCache.create()) {
IllegalStateException thrown =
- assertThrows(IllegalStateException.class, () -> PerThreadCache.create(null));
+ assertThrows(IllegalStateException.class, () -> PerThreadCache.create());
assertThat(thrown).hasMessageThat().contains("called create() twice on the same request");
}
}
@Test
- public void isAssociatedWithHttpReadonlyRequest() {
- HttpServletRequest getRequest = new FakeHttpServletRequest();
- try (PerThreadCache cache = PerThreadCache.create(getRequest)) {
- assertThat(cache.getHttpRequest()).hasValue(getRequest);
- assertThat(cache.hasReadonlyRequest()).isTrue();
- }
- }
-
- @Test
- public void isAssociatedWithHttpWriteRequest() {
- HttpServletRequest putRequest =
- new HttpServletRequestWrapper(new FakeHttpServletRequest()) {
- @Override
- public String getMethod() {
- return "PUT";
- }
- };
- try (PerThreadCache cache = PerThreadCache.create(putRequest)) {
- assertThat(cache.getHttpRequest()).hasValue(putRequest);
- assertThat(cache.hasReadonlyRequest()).isFalse();
- }
- }
-
- @Test
- public void isNotAssociatedWithHttpRequest() {
- try (PerThreadCache cache = PerThreadCache.create(null)) {
- assertThat(cache.getHttpRequest()).isEmpty();
- assertThat(cache.hasReadonlyRequest()).isFalse();
- }
- }
-
- @Test
public void enforceMaxSize() {
- try (PerThreadCache cache = PerThreadCache.create(null)) {
+ try (PerThreadCache cache = PerThreadCache.create()) {
// Fill the cache
for (int i = 0; i < 50; i++) {
PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class, i);
diff --git a/javatests/com/google/gerrit/server/events/EventJsonTest.java b/javatests/com/google/gerrit/server/events/EventJsonTest.java
index 3c9a355..c2b67c3 100644
--- a/javatests/com/google/gerrit/server/events/EventJsonTest.java
+++ b/javatests/com/google/gerrit/server/events/EventJsonTest.java
@@ -593,6 +593,24 @@
.build());
}
+ @Test
+ public void projectHeadUpdatedEvent() {
+ ProjectHeadUpdatedEvent event = new ProjectHeadUpdatedEvent();
+ event.projectName = PROJECT;
+ event.oldHead = "refs/heads/master";
+ event.newHead = REF;
+
+ assertThatJsonMap(event)
+ .isEqualTo(
+ ImmutableMap.builder()
+ .put("projectName", PROJECT)
+ .put("oldHead", "refs/heads/master")
+ .put("newHead", REF)
+ .put("type", "project-head-updated")
+ .put("eventCreatedOn", TS1)
+ .build());
+ }
+
private Supplier<AccountAttribute> newAccount(String name) {
AccountAttribute account = new AccountAttribute();
account.name = name;
diff --git a/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java b/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
index 29dbe58..6bdf80f 100644
--- a/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
+++ b/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
@@ -29,7 +29,6 @@
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;
@@ -60,7 +59,8 @@
Ref ref2 = createRefWithEmptyTreeCommit(usersRepo, 1, 1000002);
DeleteZombieCommentsRefs clean =
- new DeleteZombieCommentsRefs(new AllUsersName("All-Users"), repoManager, null);
+ new DeleteZombieCommentsRefs(
+ new AllUsersName("All-Users"), repoManager, null, (msg) -> {});
clean.execute();
/* Check that ref1 still exists, and ref2 is deleted */
@@ -81,7 +81,7 @@
int cleanupPercentage = 50;
DeleteZombieCommentsRefs clean =
new DeleteZombieCommentsRefs(
- new AllUsersName("All-Users"), repoManager, cleanupPercentage);
+ new AllUsersName("All-Users"), repoManager, cleanupPercentage, (msg) -> {});
clean.execute();
/* ref1 not deleted, ref2 deleted, ref3 not deleted because of the clean percentage */
@@ -101,7 +101,7 @@
cleanupPercentage = 70;
clean =
new DeleteZombieCommentsRefs(
- new AllUsersName("All-Users"), repoManager, cleanupPercentage);
+ new AllUsersName("All-Users"), repoManager, cleanupPercentage, (msg) -> {});
clean.execute();
@@ -137,7 +137,8 @@
.isEqualTo(goodRefs.size() + badRefs.size());
DeleteZombieCommentsRefs clean =
- new DeleteZombieCommentsRefs(new AllUsersName("All-Users"), repoManager, null);
+ new DeleteZombieCommentsRefs(
+ new AllUsersName("All-Users"), repoManager, null, (msg) -> {});
clean.execute();
assertThat(
@@ -204,14 +205,11 @@
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"), Date.from(TimeUtil.now()));
+ new PersonIdent(new PersonIdent("Foo Bar", "foo.bar@baz.com"), TimeUtil.now());
CommitBuilder cb = new CommitBuilder();
cb.setTreeId(treeId);
cb.setCommitter(committer);
diff --git a/javatests/com/google/gerrit/server/git/RepoRefCacheTest.java b/javatests/com/google/gerrit/server/git/RepoRefCacheTest.java
new file mode 100644
index 0000000..2bc6b92
--- /dev/null
+++ b/javatests/com/google/gerrit/server/git/RepoRefCacheTest.java
@@ -0,0 +1,274 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.attributes.AttributesNodeProvider;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectDatabase;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefRename;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.ReflogReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.util.FS;
+import org.junit.Test;
+
+public class RepoRefCacheTest {
+ private static final String TEST_BRANCH = "main";
+
+ @Test
+ @SuppressWarnings("resource")
+ public void repositoryUseShouldBeTrackedByRepoRefCache() throws Exception {
+ RefCache cache;
+ TestRepositoryWithRefCounting repoWithRefCounting;
+
+ try (TestRepositoryWithRefCounting repo =
+ TestRepositoryWithRefCounting.createWithBranch(TEST_BRANCH)) {
+ assertThat(repo.refCounter()).isEqualTo(1);
+ repoWithRefCounting = repo;
+ cache = new RepoRefCache(repo);
+ }
+
+ assertThat(repoWithRefCounting.refCounter()).isEqualTo(1);
+ assertThat(cache.get(Constants.R_HEADS + TEST_BRANCH)).isNotNull();
+ }
+
+ private static class TestRepositoryWithRefCounting extends Repository {
+ private int refCounter;
+
+ static TestRepositoryWithRefCounting createWithBranch(String branchName) throws Exception {
+ InMemoryRepository.Builder builder =
+ new InMemoryRepository.Builder()
+ .setRepositoryDescription(new DfsRepositoryDescription(""))
+ .setFS(FS.detect().setUserHome(null));
+ TestRepositoryWithRefCounting testRepo = new TestRepositoryWithRefCounting(builder);
+ new TestRepository<>(testRepo).branch(branchName).commit().message("").create();
+ return testRepo;
+ }
+
+ private final Repository repo;
+
+ private TestRepositoryWithRefCounting(InMemoryRepository.Builder builder) throws IOException {
+ super(builder);
+
+ repo = builder.build();
+ refCounter = 1;
+ }
+
+ public int refCounter() {
+ return refCounter;
+ }
+
+ @Override
+ public void incrementOpen() {
+ repo.incrementOpen();
+ refCounter++;
+ }
+
+ @Override
+ public void close() {
+ repo.close();
+ refCounter--;
+ }
+
+ @Override
+ public void create(boolean bare) throws IOException {}
+
+ @Override
+ public ObjectDatabase getObjectDatabase() {
+ checkIsOpen();
+ return repo.getObjectDatabase();
+ }
+
+ @Override
+ public RefDatabase getRefDatabase() {
+ RefDatabase refDatabase = repo.getRefDatabase();
+ return new RefDatabase() {
+
+ @Override
+ public int hashCode() {
+ return refDatabase.hashCode();
+ }
+
+ @Override
+ public void create() throws IOException {
+ refDatabase.create();
+ }
+
+ @Override
+ public void close() {
+ checkIsOpen();
+ refDatabase.close();
+ }
+
+ @Override
+ public boolean isNameConflicting(String name) throws IOException {
+ checkIsOpen();
+ return refDatabase.isNameConflicting(name);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return refDatabase.equals(obj);
+ }
+
+ @Override
+ public Collection<String> getConflictingNames(String name) throws IOException {
+ checkIsOpen();
+ return refDatabase.getConflictingNames(name);
+ }
+
+ @Override
+ public RefUpdate newUpdate(String name, boolean detach) throws IOException {
+ checkIsOpen();
+ return refDatabase.newUpdate(name, detach);
+ }
+
+ @Override
+ public RefRename newRename(String fromName, String toName) throws IOException {
+ checkIsOpen();
+ return refDatabase.newRename(fromName, toName);
+ }
+
+ @Override
+ public BatchRefUpdate newBatchUpdate() {
+ checkIsOpen();
+ return refDatabase.newBatchUpdate();
+ }
+
+ @Override
+ public boolean performsAtomicTransactions() {
+ checkIsOpen();
+ return refDatabase.performsAtomicTransactions();
+ }
+
+ @Override
+ public Ref exactRef(String name) throws IOException {
+ checkIsOpen();
+ return refDatabase.exactRef(name);
+ }
+
+ @Override
+ public String toString() {
+ return refDatabase.toString();
+ }
+
+ @Override
+ public Map<String, Ref> exactRef(String... refs) throws IOException {
+ checkIsOpen();
+ return refDatabase.exactRef(refs);
+ }
+
+ @Override
+ public Ref firstExactRef(String... refs) throws IOException {
+ checkIsOpen();
+ return refDatabase.firstExactRef(refs);
+ }
+
+ @Override
+ public List<Ref> getRefs() throws IOException {
+ checkIsOpen();
+ return refDatabase.getRefs();
+ }
+
+ @Override
+ public Map<String, Ref> getRefs(String prefix) throws IOException {
+ checkIsOpen();
+ return refDatabase.getRefs(prefix);
+ }
+
+ @Override
+ public List<Ref> getRefsByPrefix(String prefix) throws IOException {
+ checkIsOpen();
+ return refDatabase.getRefsByPrefix(prefix);
+ }
+
+ @Override
+ public boolean hasRefs() throws IOException {
+ checkIsOpen();
+ return refDatabase.hasRefs();
+ }
+
+ @Override
+ public List<Ref> getAdditionalRefs() throws IOException {
+ checkIsOpen();
+ return refDatabase.getAdditionalRefs();
+ }
+
+ @Override
+ public Ref peel(Ref ref) throws IOException {
+ checkIsOpen();
+ return refDatabase.peel(ref);
+ }
+
+ @Override
+ public void refresh() {
+ checkIsOpen();
+ refDatabase.refresh();
+ }
+ };
+ }
+
+ @Override
+ public StoredConfig getConfig() {
+ return repo.getConfig();
+ }
+
+ @Override
+ public AttributesNodeProvider createAttributesNodeProvider() {
+ checkIsOpen();
+ return repo.createAttributesNodeProvider();
+ }
+
+ @Override
+ public void scanForRepoChanges() throws IOException {
+ checkIsOpen();
+ }
+
+ @Override
+ public void notifyIndexChanged(boolean internal) {
+ checkIsOpen();
+ }
+
+ @Override
+ public ReflogReader getReflogReader(String refName) throws IOException {
+ checkIsOpen();
+ return repo.getReflogReader(refName);
+ }
+
+ private void checkIsOpen() {
+ if (refCounter <= 0) {
+ throw new IllegalStateException("Repository is not open (refCounter=" + refCounter + ")");
+ }
+ }
+
+ @Override
+ public String getIdentifier() {
+ return "foo";
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java b/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
index 6792703..91d5596 100644
--- a/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
+++ b/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
@@ -29,10 +29,9 @@
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.testing.TestTimeUtil;
import java.io.IOException;
+import java.time.ZoneId;
import java.util.Arrays;
-import java.util.Date;
import java.util.Optional;
-import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
@@ -56,7 +55,7 @@
// instead coming up with a replacement interface for
// VersionedMetaData/BatchMetaDataUpdate/MetaDataUpdate that is easier to use correctly.
- private static final TimeZone TZ = TimeZone.getTimeZone("America/Los_Angeles");
+ private static final ZoneId ZONE_ID = ZoneId.of("America/Los_Angeles");
private static final String DEFAULT_REF = "refs/meta/config";
private Project.NameKey project;
@@ -221,13 +220,10 @@
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", Date.from(TimeUtil.now()), TZ);
+ new PersonIdent("J. Author", "author@example.com", TimeUtil.now(), ZONE_ID);
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 54407ca..11f3528 100644
--- a/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
+++ b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
@@ -31,9 +31,8 @@
import com.google.gerrit.testing.InMemoryRepositoryManager;
import java.io.IOException;
import java.time.Instant;
-import java.util.Date;
+import java.time.ZoneId;
import java.util.Optional;
-import java.util.TimeZone;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
@@ -45,7 +44,7 @@
@Ignore
public class AbstractGroupTest {
- protected static final TimeZone TZ = TimeZone.getTimeZone("America/Los_Angeles");
+ protected static final ZoneId ZONE_ID = ZoneId.of("America/Los_Angeles");
protected static final String SERVER_ID = "server-id";
protected static final String SERVER_NAME = "Gerrit Server";
protected static final String SERVER_EMAIL = "noreply@gerritcodereview.com";
@@ -60,16 +59,13 @@
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, Date.from(TimeUtil.now()), TZ);
+ serverIdent = new PersonIdent(SERVER_NAME, SERVER_EMAIL, TimeUtil.now(), ZONE_ID);
userId = Account.id(USER_ACCOUNT_NUMBER);
userIdent = newPersonIdent(userId, serverIdent);
}
@@ -79,15 +75,12 @@
allUsersRepo.close();
}
- // 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
- : rw.parseCommit(ref.getObjectId()).getAuthorIdent().getWhen().toInstant();
+ : rw.parseCommit(ref.getObjectId()).getAuthorIdent().getWhenAsInstant();
}
}
@@ -116,11 +109,8 @@
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, Date.from(TimeUtil.now()), TZ);
+ return new PersonIdent(SERVER_NAME, SERVER_EMAIL, TimeUtil.now(), ZONE_ID);
}
protected static PersonIdent newPersonIdent(Account.Id id, PersonIdent ident) {
diff --git a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
index a8f9ff5..8c19732 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
@@ -40,9 +40,7 @@
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;
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -64,7 +62,7 @@
private final AccountGroup.Id groupId = AccountGroup.id(123);
private final AuditLogFormatter auditLogFormatter =
AuditLogFormatter.createBackedBy(ImmutableSet.of(), ImmutableSet.of(), "server-id");
- private final TimeZone timeZone = TimeZone.getTimeZone("America/Los_Angeles");
+ private final ZoneId zoneId = ZoneId.of("America/Los_Angeles");
@Before
public void setUp() throws Exception {
@@ -1044,9 +1042,6 @@
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 {
Instant committerTimestamp =
@@ -1068,23 +1063,18 @@
groupConfig.setGroupDelta(groupDelta, auditLogFormatter);
PersonIdent committerIdent =
- new PersonIdent(
- "Jane", "Jane@gerritcodereview.com", Date.from(committerTimestamp), timeZone);
+ new PersonIdent("Jane", "Jane@gerritcodereview.com", committerTimestamp, zoneId);
try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
metaDataUpdate.getCommitBuilder().setCommitter(committerIdent);
groupConfig.commit(metaDataUpdate);
}
RevCommit revCommit = getLatestCommitForGroup(groupUuid);
- assertThat(revCommit.getCommitterIdent().getWhen().getTime())
- .isEqualTo(createdOn.toEpochMilli());
- assertThat(revCommit.getCommitterIdent().getTimeZone().getRawOffset())
- .isEqualTo(timeZone.getRawOffset());
+ assertThat(revCommit.getCommitterIdent().getWhenAsInstant()).isEqualTo(createdOn);
+ assertThat(revCommit.getCommitterIdent().getZoneId().getRules().getOffset(createdOn))
+ .isEqualTo(zoneId.getRules().getOffset(createdOn));
}
- // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
- // Instants
- @SuppressWarnings("JdkObsolete")
@Test
public void timestampOfAuthorMatchesSpecifiedCreatedOnOfNewGroup() throws Exception {
Instant authorTimestamp = toInstant(LocalDate.of(2017, Month.DECEMBER, 13).atTime(15, 5, 27));
@@ -1105,16 +1095,16 @@
groupConfig.setGroupDelta(groupDelta, auditLogFormatter);
PersonIdent authorIdent =
- new PersonIdent("Jane", "Jane@gerritcodereview.com", Date.from(authorTimestamp), timeZone);
+ new PersonIdent("Jane", "Jane@gerritcodereview.com", authorTimestamp, zoneId);
try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
metaDataUpdate.getCommitBuilder().setAuthor(authorIdent);
groupConfig.commit(metaDataUpdate);
}
RevCommit revCommit = getLatestCommitForGroup(groupUuid);
- assertThat(revCommit.getAuthorIdent().getWhen().getTime()).isEqualTo(createdOn.toEpochMilli());
- assertThat(revCommit.getAuthorIdent().getTimeZone().getRawOffset())
- .isEqualTo(timeZone.getRawOffset());
+ assertThat(revCommit.getAuthorIdent().getWhenAsInstant()).isEqualTo(createdOn);
+ assertThat(revCommit.getAuthorIdent().getZoneId().getRules().getOffset(createdOn))
+ .isEqualTo(zoneId.getRules().getOffset(createdOn));
}
@Test
@@ -1149,9 +1139,6 @@
}
@Test
- // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
- // Instants
- @SuppressWarnings("JdkObsolete")
public void timestampOfCommitterMatchesSpecifiedUpdatedOnOfUpdatedGroup() throws Exception {
Instant committerTimestamp =
toInstant(LocalDate.of(2017, Month.DECEMBER, 13).atTime(15, 5, 27));
@@ -1167,24 +1154,19 @@
groupConfig.setGroupDelta(groupDelta, auditLogFormatter);
PersonIdent committerIdent =
- new PersonIdent(
- "Jane", "Jane@gerritcodereview.com", Date.from(committerTimestamp), timeZone);
+ new PersonIdent("Jane", "Jane@gerritcodereview.com", committerTimestamp, zoneId);
try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
metaDataUpdate.getCommitBuilder().setCommitter(committerIdent);
groupConfig.commit(metaDataUpdate);
}
RevCommit revCommit = getLatestCommitForGroup(groupUuid);
- assertThat(revCommit.getCommitterIdent().getWhen().getTime())
- .isEqualTo(updatedOn.toEpochMilli());
- assertThat(revCommit.getCommitterIdent().getTimeZone().getRawOffset())
- .isEqualTo(timeZone.getRawOffset());
+ assertThat(revCommit.getCommitterIdent().getWhenAsInstant()).isEqualTo(updatedOn);
+ assertThat(revCommit.getCommitterIdent().getZoneId().getRules().getOffset(updatedOn))
+ .isEqualTo(zoneId.getRules().getOffset(updatedOn));
}
@Test
- // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
- // Instants
- @SuppressWarnings("JdkObsolete")
public void timestampOfAuthorMatchesSpecifiedUpdatedOnOfUpdatedGroup() throws Exception {
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));
@@ -1199,16 +1181,16 @@
groupConfig.setGroupDelta(groupDelta, auditLogFormatter);
PersonIdent authorIdent =
- new PersonIdent("Jane", "Jane@gerritcodereview.com", Date.from(authorTimestamp), timeZone);
+ new PersonIdent("Jane", "Jane@gerritcodereview.com", authorTimestamp, zoneId);
try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
metaDataUpdate.getCommitBuilder().setAuthor(authorIdent);
groupConfig.commit(metaDataUpdate);
}
RevCommit revCommit = getLatestCommitForGroup(groupUuid);
- assertThat(revCommit.getAuthorIdent().getWhen().getTime()).isEqualTo(updatedOn.toEpochMilli());
- assertThat(revCommit.getAuthorIdent().getTimeZone().getRawOffset())
- .isEqualTo(timeZone.getRawOffset());
+ assertThat(revCommit.getAuthorIdent().getWhenAsInstant()).isEqualTo(updatedOn);
+ assertThat(revCommit.getAuthorIdent().getZoneId().getRules().getOffset(updatedOn))
+ .isEqualTo(zoneId.getRules().getOffset(updatedOn));
}
@Test
@@ -1557,13 +1539,9 @@
}
}
- // 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", Date.from(TimeUtil.now()), timeZone);
+ new PersonIdent("Gerrit Server", "noreply@gerritcodereview.com", TimeUtil.now(), zoneId);
MetaDataUpdate metaDataUpdate =
new MetaDataUpdate(
diff --git a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
index afc56ff..9d8f260 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
@@ -44,11 +44,10 @@
import com.google.gerrit.truth.ListSubject;
import com.google.gerrit.truth.OptionalSubject;
import java.io.IOException;
+import java.time.ZoneId;
import java.util.Arrays;
-import java.util.Date;
import java.util.List;
import java.util.Optional;
-import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -72,7 +71,7 @@
public class GroupNameNotesTest {
private static final String SERVER_NAME = "Gerrit Server";
private static final String SERVER_EMAIL = "noreply@gerritcodereview.com";
- private static final TimeZone TZ = TimeZone.getTimeZone("America/Los_Angeles");
+ private static final ZoneId ZONE_ID = ZoneId.of("America/Los_Angeles");
private final AccountGroup.UUID groupUuid = AccountGroup.uuid("users-XYZ");
private final AccountGroup.NameKey groupName = AccountGroup.nameKey("users");
@@ -558,11 +557,8 @@
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, Date.from(TimeUtil.now()), TZ);
+ return new PersonIdent(SERVER_NAME, SERVER_EMAIL, TimeUtil.now(), ZONE_ID);
}
private static ObjectId getNoteKey(GroupReference g) {
diff --git a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index 222be83..d7c779e 100644
--- a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -68,8 +68,7 @@
import com.google.inject.Inject;
import com.google.inject.Injector;
import java.time.Instant;
-import java.util.Date;
-import java.util.TimeZone;
+import java.time.ZoneId;
import java.util.concurrent.ExecutorService;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
@@ -85,7 +84,7 @@
@Ignore
@RunWith(ConfigSuite.class)
public abstract class AbstractChangeNotesTest {
- private static final TimeZone TZ = TimeZone.getTimeZone("America/Los_Angeles");
+ private static final ZoneId ZONE_ID = ZoneId.of("America/Los_Angeles");
@ConfigSuite.Parameter public Config testConfig;
@@ -115,15 +114,11 @@
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", Date.from(TimeUtil.now()), TZ);
+ serverIdent = new PersonIdent("Gerrit Server", "noreply@gerrit.com", TimeUtil.now(), ZONE_ID);
project = Project.nameKey("test-project");
repoManager = new InMemoryRepositoryManager();
repo = repoManager.createRepository(project);
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNoteJsonTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNoteJsonTest.java
new file mode 100644
index 0000000..24e28f3
--- /dev/null
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNoteJsonTest.java
@@ -0,0 +1,127 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.notedb;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+
+import com.google.gson.Gson;
+import com.google.inject.TypeLiteral;
+import java.util.Optional;
+import org.junit.Test;
+
+public class ChangeNoteJsonTest {
+ private final Gson gson = new ChangeNoteJson().getGson();
+
+ static class Child {
+ Optional<String> optionalValue;
+ }
+
+ static class Parent {
+ Optional<Child> optionalChild;
+ }
+
+ @Test
+ public void shouldSerializeAndDeserializeEmptyOptional() {
+ // given
+ Optional<?> empty = Optional.empty();
+
+ // when
+ String json = gson.toJson(empty);
+
+ // then
+ assertThat(json).isEqualTo("{}");
+
+ // and when
+ Optional<?> result = gson.fromJson(json, Optional.class);
+
+ // and then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void shouldSerializeAndDeserializeNonEmptyOptional() {
+ // given
+ String value = "foo";
+ Optional<String> nonEmpty = Optional.of(value);
+
+ // when
+ String json = gson.toJson(nonEmpty);
+
+ // then
+ assertThat(json).isEqualTo("{\n \"value\": \"" + value + "\"\n}");
+
+ // and when
+ Optional<String> result = gson.fromJson(json, new TypeLiteral<Optional<String>>() {}.getType());
+
+ // and then
+ assertThat(result).isPresent();
+ assertThat(result.get()).isEqualTo(value);
+ }
+
+ @Test
+ public void shouldSerializeAndDeserializeNestedNonEmptyOptional() {
+ String value = "foo";
+ Child fooChild = new Child();
+ fooChild.optionalValue = Optional.of(value);
+ Parent parent = new Parent();
+ parent.optionalChild = Optional.of(fooChild);
+
+ String json = gson.toJson(parent);
+
+ assertThat(json)
+ .isEqualTo(
+ "{\n"
+ + " \"optionalChild\": {\n"
+ + " \"value\": {\n"
+ + " \"optionalValue\": {\n"
+ + " \"value\": \"foo\"\n"
+ + " }\n"
+ + " }\n"
+ + " }\n"
+ + "}");
+
+ Parent result = gson.fromJson(json, new TypeLiteral<Parent>() {}.getType());
+
+ assertThat(result.optionalChild).isPresent();
+ assertThat(result.optionalChild.get().optionalValue).isPresent();
+ assertThat(result.optionalChild.get().optionalValue.get()).isEqualTo(value);
+ }
+
+ @Test
+ public void shouldSerializeAndDeserializeNestedEmptyOptional() {
+ Child fooChild = new Child();
+ fooChild.optionalValue = Optional.empty();
+ Parent parent = new Parent();
+ parent.optionalChild = Optional.of(fooChild);
+
+ String json = gson.toJson(parent);
+
+ assertThat(json)
+ .isEqualTo(
+ "{\n"
+ + " \"optionalChild\": {\n"
+ + " \"value\": {\n"
+ + " \"optionalValue\": {}\n"
+ + " }\n"
+ + " }\n"
+ + "}");
+
+ Parent result = gson.fromJson(json, new TypeLiteral<Parent>() {}.getType());
+
+ assertThat(result.optionalChild).isPresent();
+ assertThat(result.optionalChild.get().optionalValue).isEmpty();
+ }
+}
diff --git a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index 5e2e1f2..9d02067 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -27,7 +27,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.testing.TestChanges;
-import java.util.TimeZone;
+import java.time.ZoneId;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -35,9 +35,6 @@
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);
@@ -70,14 +67,15 @@
PersonIdent author = commit.getAuthorIdent();
assertThat(author.getName()).isEqualTo("Gerrit User 1");
assertThat(author.getEmailAddress()).isEqualTo("1@gerrit");
- assertThat(author.getWhen().getTime()).isEqualTo(c.getCreatedOn().toEpochMilli() + 1000);
- assertThat(author.getTimeZone()).isEqualTo(TimeZone.getTimeZone("GMT-7:00"));
+ assertThat(author.getWhenAsInstant().toEpochMilli())
+ .isEqualTo(c.getCreatedOn().toEpochMilli() + 1000);
+ assertThat(author.getZoneId()).isEqualTo(ZoneId.of("GMT-7"));
PersonIdent committer = commit.getCommitterIdent();
assertThat(committer.getName()).isEqualTo("Gerrit Server");
assertThat(committer.getEmailAddress()).isEqualTo("noreply@gerrit.com");
- assertThat(committer.getWhen().getTime()).isEqualTo(author.getWhen().getTime());
- assertThat(committer.getTimeZone()).isEqualTo(author.getTimeZone());
+ assertThat(committer.getWhenAsInstant()).isEqualTo(author.getWhenAsInstant());
+ assertThat(committer.getZoneId()).isEqualTo(author.getZoneId());
}
@Test
@@ -145,9 +143,6 @@
}
@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);
@@ -189,14 +184,15 @@
PersonIdent author = commit.getAuthorIdent();
assertThat(author.getName()).isEqualTo("Gerrit User 1");
assertThat(author.getEmailAddress()).isEqualTo("1@gerrit");
- assertThat(author.getWhen().getTime()).isEqualTo(c.getCreatedOn().toEpochMilli() + 2000);
- assertThat(author.getTimeZone()).isEqualTo(TimeZone.getTimeZone("GMT-7:00"));
+ assertThat(author.getWhenAsInstant().toEpochMilli())
+ .isEqualTo(c.getCreatedOn().toEpochMilli() + 2000);
+ assertThat(author.getZoneId()).isEqualTo(ZoneId.of("GMT-7"));
PersonIdent committer = commit.getCommitterIdent();
assertThat(committer.getName()).isEqualTo("Gerrit Server");
assertThat(committer.getEmailAddress()).isEqualTo("noreply@gerrit.com");
- assertThat(committer.getWhen().getTime()).isEqualTo(author.getWhen().getTime());
- assertThat(committer.getTimeZone()).isEqualTo(author.getTimeZone());
+ assertThat(committer.getWhenAsInstant()).isEqualTo(author.getWhenAsInstant());
+ assertThat(committer.getZoneId()).isEqualTo(author.getZoneId());
}
@Test
diff --git a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
index 051ea2d..5e6803e 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
@@ -49,7 +49,6 @@
import com.google.inject.Inject;
import java.time.Instant;
import java.util.Arrays;
-import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -321,9 +320,6 @@
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();
@@ -332,8 +328,8 @@
new PersonIdent(
changeOwner.getName(),
changeNoteUtil.getAccountIdAsEmailAddress(changeOwner.getAccountId()),
- Date.from(when),
- serverIdent.getTimeZone());
+ when,
+ serverIdent.getZoneId());
RevCommit invalidUpdateCommit =
writeUpdate(
RefNames.changeMetaRef(c.getId()),
@@ -374,8 +370,8 @@
assertThat(fixedUpdateCommit.getAuthorIdent().getName())
.isEqualTo("Gerrit User " + changeOwner.getAccountId());
assertThat(originalAuthorIdent.getEmailAddress()).isEqualTo(fixedAuthorIdent.getEmailAddress());
- assertThat(originalAuthorIdent.getWhen().getTime())
- .isEqualTo(fixedAuthorIdent.getWhen().getTime());
+ assertThat(originalAuthorIdent.getWhenAsInstant())
+ .isEqualTo(fixedAuthorIdent.getWhenAsInstant());
assertThat(originalAuthorIdent.getTimeZone()).isEqualTo(fixedAuthorIdent.getTimeZone());
assertThat(invalidUpdateCommit.getFullMessage()).isEqualTo(fixedUpdateCommit.getFullMessage());
assertThat(invalidUpdateCommit.getCommitterIdent())
@@ -453,9 +449,6 @@
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();
@@ -502,7 +495,7 @@
BackfillResult result = rewriter.backfillProject(project, repo, options);
assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
- Instant updateTimestamp = serverIdent.getWhen().toInstant();
+ Instant updateTimestamp = serverIdent.getWhenAsInstant();
ImmutableList<ReviewerStatusUpdate> expectedReviewerUpdates =
ImmutableList.of(
ReviewerStatusUpdate.create(
@@ -539,9 +532,6 @@
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();
@@ -589,7 +579,7 @@
BackfillResult result = rewriter.backfillProject(project, repo, options);
assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
- Instant updateTimestamp = serverIdent.getWhen().toInstant();
+ Instant updateTimestamp = serverIdent.getWhenAsInstant();
ImmutableList<ReviewerStatusUpdate> expectedReviewerUpdates =
ImmutableList.of(
ReviewerStatusUpdate.create(
@@ -669,9 +659,6 @@
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();
@@ -722,7 +709,7 @@
BackfillResult result = rewriter.backfillProject(project, repo, options);
assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
- Instant updateTimestamp = serverIdent.getWhen().toInstant();
+ Instant updateTimestamp = serverIdent.getWhenAsInstant();
ImmutableList<PatchSetApproval> expectedApprovals =
ImmutableList.of(
PatchSetApproval.builder()
@@ -806,9 +793,6 @@
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();
@@ -862,7 +846,7 @@
BackfillResult result = rewriter.backfillProject(project, repo, options);
assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
- Instant updateTimestamp = serverIdent.getWhen().toInstant();
+ Instant updateTimestamp = serverIdent.getWhenAsInstant();
ImmutableList<PatchSetApproval> expectedApprovals =
ImmutableList.of(
PatchSetApproval.builder()
@@ -932,18 +916,12 @@
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();
PersonIdent invalidAuthorIdent =
new PersonIdent(
- changeOwner.getName(),
- "server@" + serverId,
- Date.from(TimeUtil.now()),
- serverIdent.getTimeZone());
+ changeOwner.getName(), "server@" + serverId, TimeUtil.now(), serverIdent.getZoneId());
writeUpdate(
RefNames.changeMetaRef(c.getId()),
getChangeUpdateBody(
@@ -1188,9 +1166,6 @@
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();
@@ -1271,7 +1246,7 @@
BackfillResult result = rewriter.backfillProject(project, repo, options);
assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
notesBeforeRewrite.getAttentionSetUpdates();
- Instant updateTimestamp = serverIdent.getWhen().toInstant();
+ Instant updateTimestamp = serverIdent.getWhenAsInstant();
ImmutableList<AttentionSetUpdate> attentionSetUpdatesBeforeRewrite =
ImmutableList.of(
AttentionSetUpdate.createFromRead(
@@ -1569,9 +1544,6 @@
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();
@@ -1579,8 +1551,8 @@
new PersonIdent(
changeOwner.getName(),
changeNoteUtil.getAccountIdAsEmailAddress(changeOwner.getAccountId()),
- Date.from(TimeUtil.now()),
- serverIdent.getTimeZone());
+ TimeUtil.now(),
+ serverIdent.getZoneId());
String changeOwnerIdentToFix = getAccountIdentToFix(changeOwner.getAccount());
writeUpdate(
RefNames.changeMetaRef(c.getId()),
@@ -2281,9 +2253,6 @@
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();
@@ -2293,8 +2262,8 @@
new PersonIdent(
changeOwner.getName(),
changeNoteUtil.getAccountIdAsEmailAddress(changeOwner.getAccountId()),
- Date.from(when),
- serverIdent.getTimeZone());
+ when,
+ serverIdent.getZoneId());
RevCommit invalidUpdateCommit =
writeUpdate(
diff --git a/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java b/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
index 788703c..1c28690 100644
--- a/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
+++ b/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
@@ -32,7 +32,6 @@
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;
@@ -256,14 +255,11 @@
: 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"), Date.from(TimeUtil.now()));
+ new PersonIdent(new PersonIdent("Foo Bar", "foo.bar@baz.com"), TimeUtil.now());
CommitBuilder cb = new CommitBuilder();
cb.setTreeId(treeId);
cb.setCommitter(committer);
diff --git a/javatests/com/google/gerrit/server/patch/MagicFileTest.java b/javatests/com/google/gerrit/server/patch/MagicFileTest.java
index 21ea641..b0050b0 100644
--- a/javatests/com/google/gerrit/server/patch/MagicFileTest.java
+++ b/javatests/com/google/gerrit/server/patch/MagicFileTest.java
@@ -22,9 +22,8 @@
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.Month;
+import java.time.ZoneId;
import java.time.ZoneOffset;
-import java.util.Date;
-import java.util.TimeZone;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
@@ -95,9 +94,6 @@
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"));
@@ -107,20 +103,12 @@
Instant authorTime =
LocalDateTime.of(2020, Month.APRIL, 23, 19, 30, 27).atZone(ZoneOffset.UTC).toInstant();
PersonIdent author =
- new PersonIdent(
- "Alfred",
- "alfred@example.com",
- Date.from(authorTime),
- TimeZone.getTimeZone(ZoneOffset.UTC));
+ new PersonIdent("Alfred", "alfred@example.com", authorTime, ZoneId.of("UTC"));
Instant committerTime =
LocalDateTime.of(2021, Month.JANUARY, 6, 5, 12, 55).atZone(ZoneOffset.UTC).toInstant();
PersonIdent committer =
- new PersonIdent(
- "Luise",
- "luise@example.com",
- Date.from(committerTime),
- TimeZone.getTimeZone(ZoneOffset.UTC));
+ new PersonIdent("Luise", "luise@example.com", committerTime, ZoneId.of("UTC"));
ObjectId commit =
testRepo
@@ -149,9 +137,6 @@
}
}
- // 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"));
@@ -161,20 +146,12 @@
Instant authorTime =
LocalDateTime.of(2020, Month.APRIL, 23, 19, 30, 27).atZone(ZoneOffset.UTC).toInstant();
PersonIdent author =
- new PersonIdent(
- "Alfred",
- "alfred@example.com",
- Date.from(authorTime),
- TimeZone.getTimeZone(ZoneOffset.UTC));
+ new PersonIdent("Alfred", "alfred@example.com", authorTime, ZoneId.of("UTC"));
Instant committerTime =
LocalDateTime.of(2021, Month.JANUARY, 6, 5, 12, 55).atZone(ZoneOffset.UTC).toInstant();
PersonIdent committer =
- new PersonIdent(
- "Luise",
- "luise@example.com",
- Date.from(committerTime),
- TimeZone.getTimeZone(ZoneOffset.UTC));
+ new PersonIdent("Luise", "luise@example.com", committerTime, ZoneId.of("UTC"));
RevCommit parent =
testRepo.commit().message("Parent subject\n\nParent further details.").create();
@@ -208,9 +185,6 @@
}
}
- // 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"));
@@ -220,20 +194,12 @@
Instant authorTime =
LocalDateTime.of(2020, Month.APRIL, 23, 19, 30, 27).atZone(ZoneOffset.UTC).toInstant();
PersonIdent author =
- new PersonIdent(
- "Alfred",
- "alfred@example.com",
- Date.from(authorTime),
- TimeZone.getTimeZone(ZoneOffset.UTC));
+ new PersonIdent("Alfred", "alfred@example.com", authorTime, ZoneId.of("UTC"));
Instant committerTime =
LocalDateTime.of(2021, Month.JANUARY, 6, 5, 12, 55).atZone(ZoneOffset.UTC).toInstant();
PersonIdent committer =
- new PersonIdent(
- "Luise",
- "luise@example.com",
- Date.from(committerTime),
- TimeZone.getTimeZone(ZoneOffset.UTC));
+ new PersonIdent("Luise", "luise@example.com", committerTime, ZoneId.of("UTC"));
RevCommit parent1 = testRepo.commit().message("Parent 1\n\nExplanation 1.").create();
RevCommit parent2 = testRepo.commit().message("Parent 2\n\nExplanation 2.").create();
diff --git a/javatests/com/google/gerrit/server/project/SubmitRequirementNameValidatorTest.java b/javatests/com/google/gerrit/server/project/SubmitRequirementNameValidatorTest.java
new file mode 100644
index 0000000..98ee71d
--- /dev/null
+++ b/javatests/com/google/gerrit/server/project/SubmitRequirementNameValidatorTest.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link SubmitRequirementsUtil#validateName(String)}. */
+@RunWith(JUnit4.class)
+public class SubmitRequirementNameValidatorTest {
+ @Test
+ public void canStartWithSmallLetter() throws Exception {
+ SubmitRequirementsUtil.validateName("abc");
+ }
+
+ @Test
+ public void canStartWithCapitalLetter() throws Exception {
+ SubmitRequirementsUtil.validateName("Abc");
+ }
+
+ @Test
+ public void canBeEqualToOneLetter() throws Exception {
+ SubmitRequirementsUtil.validateName("a");
+ }
+
+ @Test
+ public void cannotStartWithNumber() throws Exception {
+ assertThrows(
+ IllegalArgumentException.class, () -> SubmitRequirementsUtil.validateName("98abc"));
+ }
+
+ @Test
+ public void cannotStartWithHyphen() throws Exception {
+ assertThrows(IllegalArgumentException.class, () -> SubmitRequirementsUtil.validateName("-abc"));
+ }
+
+ @Test
+ public void cannotContainNonAlphanumericOrHyphen() throws Exception {
+ assertThrows(
+ IllegalArgumentException.class, () -> SubmitRequirementsUtil.validateName("a&^bc"));
+ }
+}
diff --git a/javatests/com/google/gerrit/server/update/BUILD b/javatests/com/google/gerrit/server/update/BUILD
index 4fe4ab04..6d96c10 100644
--- a/javatests/com/google/gerrit/server/update/BUILD
+++ b/javatests/com/google/gerrit/server/update/BUILD
@@ -1,8 +1,8 @@
load("//tools/bzl:junit.bzl", "junit_tests")
junit_tests(
- name = "small_tests",
- size = "small",
+ name = "update_tests",
+ size = "medium",
srcs = glob(["*.java"]),
runtime_deps = [
"//java/com/google/gerrit/lucene",
diff --git a/plugins/BUILD b/plugins/BUILD
index 7862b1c..32efa3e 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -64,6 +64,7 @@
"//java/com/google/gerrit/server/logging",
"//java/com/google/gerrit/server/schema",
"//java/com/google/gerrit/server/util/time",
+ "//java/com/google/gerrit/proto",
"//java/com/google/gerrit/util/cli",
"//java/com/google/gerrit/util/http",
"//java/com/google/gerrit/util/logging",
diff --git a/plugins/replication b/plugins/replication
index fd3b732..bdc7e6b 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit fd3b732959f964763117be5fdff78ee40ed211fa
+Subproject commit bdc7e6b3255965d0e4415a978031222a683b38d8
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index c5c262b..732a82c 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -240,34 +240,6 @@
the "Before launch" section for IntelliJ. This is a temporary problem until
typescript migration is complete.
-## Running Templates Test
-The templates test validates polymer templates. The test convert polymer
-templates into a plain typescript code and then run TS compiler. The test fails
-if TS compiler reports errors; in this case you will see TS errors in
-the log/output. Gerrit-CI automatically runs templates test.
-
-**Note**: Files defined in `ignore_templates_list` (`polygerrit-ui/app/BUILD`)
-are excluded from code generation and checking. If you don't know how to fix
-a problem, you can add a problematic template in the list.
-
-* To run test locally, use npm command:
-``` sh
-npm run polytest
-```
-
-* Often, the output from the previous command is not clear (cryptic TS errors).
-In this case, run the command
-```sh
-npm run polytest:dev
-```
-This command (re)creates the `polygerrit-ui/app/tmpl_out` directory and put
-generated files into it. For each polygerrit .ts file there is a generated file
-in the `tmpl_out` directory. If an original file doesn't contain a polymer
-template, the generated file is empty.
-
-You can open a problematic file in IDE and fix the problem. Ensure, that IDE
-uses `polygerrit-ui/app/tsconfig.json` as a project (usually, this is default).
-
### Generated file overview
A generated file starts with imports followed by a static content with
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index 6788aa3..430e770 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -38,10 +38,16 @@
* If the weblinks-only parameter is specified, only the web_links field is set.
*/
export declare interface DiffInfo {
- /** Meta information about the file on side A as a DiffFileMetaInfo entity. */
- meta_a: DiffFileMetaInfo;
- /** Meta information about the file on side B as a DiffFileMetaInfo entity. */
- meta_b: DiffFileMetaInfo;
+ /**
+ * Meta information about the file on side A as a DiffFileMetaInfo entity.
+ * Not set when change_type is ADDED.
+ */
+ meta_a?: DiffFileMetaInfo;
+ /**
+ * Meta information about the file on side B as a DiffFileMetaInfo entity.
+ * Not set when change_type is DELETED.
+ */
+ meta_b?: DiffFileMetaInfo;
/** The type of change (ADDED, MODIFIED, DELETED, RENAMED COPIED, REWRITE). */
change_type: ChangeType;
/** Intraline status (OK, ERROR, TIMEOUT). */
@@ -167,7 +173,7 @@
* Indicates the range (line numbers) on the other side of the comparison
* where the code related to the current chunk came from/went to.
*/
- range: {
+ range?: {
start: number;
end: number;
};
@@ -334,6 +340,7 @@
lineNum: LineNumber;
}
+// TODO: Currently unused and not fired.
export declare interface RenderProgressEventDetail {
linesRendered: number;
}
diff --git a/polygerrit-ui/app/api/rest-api.ts b/polygerrit-ui/app/api/rest-api.ts
index 72712ff..685151b 100644
--- a/polygerrit-ui/app/api/rest-api.ts
+++ b/polygerrit-ui/app/api/rest-api.ts
@@ -130,7 +130,7 @@
export enum InheritedBooleanInfoConfiguredValue {
TRUE = 'TRUE',
FALSE = 'FALSE',
- INHERITED = 'INHERITED',
+ INHERIT = 'INHERIT',
}
/**
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
index 8fa2e90..2c83ed3 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts
@@ -403,9 +403,9 @@
permission: PermissionArrayItem<EditablePermissionInfo>
): string | undefined {
if (this.section?.id === GLOBAL_NAME) {
- return this.capabilities?.[permission.id].name;
+ return this.capabilities?.[permission.id]?.name;
} else if (AccessPermissions[permission.id]) {
- return AccessPermissions[permission.id].name;
+ return AccessPermissions[permission.id]?.name;
} else if (permission.value.label) {
let behalfOf = '';
if (permission.id.startsWith('labelAs-')) {
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.ts b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.ts
index f4d81c4..1c4c437 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.ts
@@ -224,6 +224,12 @@
element.capabilities![permission.id].name
);
+ permission = {
+ id: 'non-existent' as GitRef,
+ value: {rules: {}},
+ };
+ assert.isUndefined(element.computePermissionName(permission));
+
element.section.id = 'refs/for/*' as GitRef;
permission = {
id: 'abandon' as GitRef,
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 9b01735..4ef307d 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
@@ -272,7 +272,7 @@
return true;
case InheritedBooleanInfoConfiguredValue.FALSE:
return false;
- case InheritedBooleanInfoConfiguredValue.INHERITED:
+ case InheritedBooleanInfoConfiguredValue.INHERIT:
return !!config.inherited_value;
default:
return false;
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 933b300..b4ebd3c 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
@@ -40,12 +40,9 @@
ChangeInfo,
ServerInfo,
AccountInfo,
- QuickLabelInfo,
Timestamp,
} from '../../../types/common';
import {hasOwnProperty, assertIsDefined} from '../../../utils/common-util';
-import {pluralize} from '../../../utils/string-util';
-import {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';
@@ -248,22 +245,6 @@
.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);
@@ -618,32 +599,13 @@
}
private renderChangeLabels(labelName: string) {
- if (showNewSubmitRequirements(this.flagsService, this.change)) {
- return html` <td class="cell label requirement">
- <gr-change-list-column-requirement
- .change=${this.change}
- .labelName=${labelName}
- >
- </gr-change-list-column-requirement>
- </td>`;
- }
- return html`
- <td
- title=${this.computeLabelTitle(labelName)}
- class=${this.computeLabelClass(labelName)}
+ return html` <td class="cell label requirement">
+ <gr-change-list-column-requirement
+ .change=${this.change}
+ .labelName=${labelName}
>
- ${this.renderChangeHasLabelIcon(labelName)}
- </td>
- `;
- }
-
- private renderChangeHasLabelIcon(labelName: string) {
- if (this.computeLabelIcon(labelName) === '')
- return html`<span>${this.computeLabelValue(labelName)}</span>`;
-
- return html`
- <iron-icon icon=${this.computeLabelIcon(labelName)}></iron-icon>
- `;
+ </gr-change-list-column-requirement>
+ </td>`;
}
private renderChangePluginEndpoint(pluginEndpointName: string) {
@@ -676,118 +638,6 @@
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 = this.change?.unresolved_comment_count ?? 0;
- titleParts.push(pluralize(num, 'unresolved comment'));
- }
- const significantLabel =
- label.rejected || label.approved || label.disliked || label.recommended;
- if (significantLabel?.name) {
- titleParts.push(`${labelName} by ${significantLabel.name}`);
- }
- if (titleParts.length > 0) {
- return titleParts.join(',\n');
- }
- return labelName;
- }
-
- // private but used in test
- computeLabelClass(labelName: string) {
- const classes = ['cell', 'label'];
- const category = this.computeLabelCategory(labelName);
- switch (category) {
- case LabelCategory.NOT_APPLICABLE:
- classes.push('u-gray-background');
- break;
- case LabelCategory.APPROVED:
- classes.push('u-green');
- break;
- case LabelCategory.POSITIVE:
- classes.push('u-monospace');
- classes.push('u-green');
- break;
- case LabelCategory.NEGATIVE:
- classes.push('u-monospace');
- classes.push('u-red');
- break;
- case LabelCategory.REJECTED:
- classes.push('u-red');
- break;
- }
- return classes.sort().join(' ');
- }
-
- // private but used in test
- computeLabelIcon(labelName: string): string {
- const category = this.computeLabelCategory(labelName);
- switch (category) {
- case LabelCategory.APPROVED:
- return 'gr-icons:check';
- case LabelCategory.UNRESOLVED_COMMENTS:
- return 'gr-icons:comment';
- case LabelCategory.REJECTED:
- return 'gr-icons:close';
- default:
- return '';
- }
- }
-
- // private but used in test
- computeLabelCategory(labelName: string) {
- const label: QuickLabelInfo | undefined = this.change?.labels?.[labelName];
- if (!label) {
- return LabelCategory.NOT_APPLICABLE;
- }
- if (label.rejected) {
- return LabelCategory.REJECTED;
- }
- if (label.value && label.value < 0) {
- return LabelCategory.NEGATIVE;
- }
- if (this.change?.unresolved_comment_count && labelName === 'Code-Review') {
- return LabelCategory.UNRESOLVED_COMMENTS;
- }
- if (label.approved) {
- return LabelCategory.APPROVED;
- }
- if (label.value && label.value > 0) {
- return LabelCategory.POSITIVE;
- }
- return LabelCategory.NEUTRAL;
- }
-
- // 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 '';
- case LabelCategory.APPROVED:
- return '\u2713'; // ✓
- case LabelCategory.POSITIVE:
- return `+${label?.value}`;
- case LabelCategory.NEUTRAL:
- return '';
- case LabelCategory.UNRESOLVED_COMMENTS:
- return 'u';
- case LabelCategory.NEGATIVE:
- return `${label?.value}`;
- case LabelCategory.REJECTED:
- return '\u2715'; // ✕
- default:
- return '';
- }
- }
-
private computeRepoUrl() {
if (!this.change) return '';
return GerritNav.getUrlForProjectChanges(
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 e0ce6a8..7216249 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
@@ -48,7 +48,7 @@
import {StandardLabels} from '../../../utils/label-util';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import './gr-change-list-item';
-import {GrChangeListItem, LabelCategory} from './gr-change-list-item';
+import {GrChangeListItem} from './gr-change-list-item';
import {
DIProviderElement,
wrapInProvider,
@@ -92,209 +92,6 @@
await element.updateComplete;
});
- test('computeLabelCategory', () => {
- element.change = {
- ...change,
- labels: {},
- };
- assert.equal(
- element.computeLabelCategory('Verified'),
- LabelCategory.NOT_APPLICABLE
- );
- element.change.labels = {Verified: {approved: account, value: 1}};
- assert.equal(
- element.computeLabelCategory('Verified'),
- LabelCategory.APPROVED
- );
- element.change.labels = {Verified: {rejected: account, value: -1}};
- assert.equal(
- element.computeLabelCategory('Verified'),
- LabelCategory.REJECTED
- );
- element.change.labels = {'Code-Review': {approved: account, value: 1}};
- element.change.unresolved_comment_count = 1;
- assert.equal(
- element.computeLabelCategory('Code-Review'),
- LabelCategory.UNRESOLVED_COMMENTS
- );
- element.change.labels = {'Code-Review': {value: 1}};
- element.change.unresolved_comment_count = 0;
- assert.equal(
- element.computeLabelCategory('Code-Review'),
- LabelCategory.POSITIVE
- );
- element.change.labels = {'Code-Review': {value: -1}};
- assert.equal(
- element.computeLabelCategory('Code-Review'),
- LabelCategory.NEGATIVE
- );
- element.change.labels = {'Code-Review': {value: -1}};
- assert.equal(
- element.computeLabelCategory('Verified'),
- LabelCategory.NOT_APPLICABLE
- );
- });
-
- test('computeLabelClass', () => {
- element.change = {
- ...change,
- labels: {},
- };
- assert.equal(
- 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('Code-Review'),
- 'cell label u-green u-monospace'
- );
- element.change.labels = {'Code-Review': {value: -1}};
- assert.equal(
- element.computeLabelClass('Code-Review'),
- 'cell label u-monospace u-red'
- );
- element.change.labels = {'Code-Review': {value: -1}};
- assert.equal(
- element.computeLabelClass('Verified'),
- 'cell label u-gray-background'
- );
- });
-
- 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('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('Code-Review'),
- 'Code-Review by Diffy'
- );
-
- element.change.labels = {
- 'Code-Review': {recommended: {name: 'Diffy'}, value: 1},
- };
- assert.equal(
- element.computeLabelTitle('Code-Review'),
- 'Code-Review by Diffy'
- );
-
- element.change.labels = {
- 'Code-Review': {recommended: {name: 'Diffy'}, rejected: {name: 'Admin'}},
- };
- assert.equal(
- element.computeLabelTitle('Code-Review'),
- 'Code-Review by Admin'
- );
-
- element.change.labels = {
- 'Code-Review': {approved: {name: 'Diffy'}, rejected: {name: 'Admin'}},
- };
- assert.equal(
- 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('Code-Review'),
- 'Code-Review by Admin'
- );
-
- element.change.labels = {
- 'Code-Review': {
- approved: {name: 'Diffy'},
- disliked: {name: 'Admin'},
- value: -1,
- },
- };
- assert.equal(
- 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('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('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('Code-Review'),
- '2 unresolved comments'
- );
- });
-
- 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', () => {
- 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 () => {
element.visibleChangeTableColumns = [
ColumnNames.SUBJECT,
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
index 24719e8..34080a1 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
@@ -10,7 +10,7 @@
import {configModelToken} from '../../../models/config/config-model';
import {resolve} from '../../../models/dependency';
import {
- AccountInfo,
+ AccountDetailInfo,
ChangeInfo,
NumericChangeId,
ServerInfo,
@@ -30,8 +30,14 @@
import {allSettled} from '../../../utils/async-util';
import {listForSentence} from '../../../utils/string-util';
import {getDisplayName} from '../../../utils/display-name-util';
-import {AccountInputDetail} from '../../shared/gr-account-list/gr-account-list';
+import {
+ AccountInput,
+ AccountInputDetail,
+} from '../../shared/gr-account-list/gr-account-list';
import '@polymer/iron-icon/iron-icon';
+import {getReplyByReason} from '../../../utils/attention-set-util';
+import {intersection} from '../../../utils/common-util';
+import {accountOrGroupKey} from '../../../utils/account-util';
@customElement('gr-change-list-reviewer-flow')
export class GrChangeListReviewerFlow extends LitElement {
@@ -40,7 +46,7 @@
// contents are given to gr-account-lists to mutate
@state() private updatedAccountsByReviewerState: Map<
ReviewerState,
- AccountInfo[]
+ AccountInput[]
> = new Map([
[ReviewerState.REVIEWER, []],
[ReviewerState.CC, []],
@@ -72,6 +78,8 @@
private isLoggedIn = false;
+ private account?: AccountDetailInfo;
+
static override get styles() {
return css`
gr-dialog {
@@ -125,6 +133,11 @@
() => getAppContext().userModel.loggedIn$,
isLoggedIn => (this.isLoggedIn = isLoggedIn)
);
+ subscribe(
+ this,
+ () => getAppContext().userModel.account$,
+ account => (this.account = account)
+ );
}
override render() {
@@ -244,12 +257,11 @@
.filter(account => account?._account_id !== undefined);
return this.updatedAccountsByReviewerState
.get(updatedReviewerState)!
- .filter(
- account =>
- account._account_id !== undefined &&
- accountsInCurrentState.some(
- otherAccount => otherAccount._account_id === account._account_id
- )
+ .filter(account =>
+ accountsInCurrentState.some(
+ otherAccount =>
+ accountOrGroupKey(otherAccount) === accountOrGroupKey(account)
+ )
)
.map(reviewer => getDisplayName(this.serverConfig, reviewer));
}
@@ -292,7 +304,6 @@
reviewerState: ReviewerState,
event: CustomEvent<AccountInputDetail>
) {
- const account = event.detail.account as AccountInfo;
const oppositeReviewerState =
reviewerState === ReviewerState.CC
? ReviewerState.REVIEWER
@@ -301,7 +312,7 @@
oppositeReviewerState
)!;
const oppositeUpdatedAccountIndex = oppositeUpdatedAccounts.findIndex(
- acc => acc._account_id === account._account_id
+ acc => accountOrGroupKey(acc) === accountOrGroupKey(event.detail.account)
);
if (oppositeUpdatedAccountIndex >= 0) {
oppositeUpdatedAccounts.splice(oppositeUpdatedAccountIndex, 1);
@@ -335,7 +346,8 @@
])
);
const inFlightActions = this.getBulkActionsModel().addReviewers(
- this.updatedAccountsByReviewerState
+ this.updatedAccountsByReviewerState,
+ getReplyByReason(this.account, this.serverConfig)
);
await allSettled(
@@ -383,13 +395,7 @@
const reviewersPerChange = this.selectedChanges.map(
change => change.reviewers[reviewerState] ?? []
);
- if (reviewersPerChange.length === 0) {
- return [];
- }
- // Gets reviewers present in all changes
- return reviewersPerChange.reduce((a, b) =>
- a.filter(reviewer => b.includes(reviewer))
- );
+ return intersection(reviewersPerChange);
}
private createSuggestionsProvider(
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
index edcad8f..4a142e4 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow_test.ts
@@ -5,7 +5,7 @@
*/
import {fixture, html} from '@open-wc/testing-helpers';
import {SinonStubbedMember} from 'sinon';
-import {AccountInfo, ReviewerState} from '../../../api/rest-api';
+import {AccountInfo, GroupInfo, ReviewerState} from '../../../api/rest-api';
import {
BulkActionsModel,
bulkActionsModelToken,
@@ -17,6 +17,7 @@
import {
createAccountWithIdNameAndEmail,
createChange,
+ createGroupInfo,
} from '../../../test/test-data-generators';
import {
MockPromise,
@@ -43,6 +44,7 @@
createAccountWithIdNameAndEmail(4),
createAccountWithIdNameAndEmail(5),
];
+const groups: GroupInfo[] = [createGroupInfo('groupId')];
const changes: ChangeInfo[] = [
{
...createChange(),
@@ -225,7 +227,7 @@
dialog,
'gr-account-list#cc-list'
);
- reviewerList.accounts.push(accounts[2]);
+ reviewerList.accounts.push(accounts[2], groups[0]);
ccList.accounts.push(accounts[5]);
await flush();
dialog.confirmButton!.click();
@@ -243,8 +245,21 @@
{
reviewers: [
{reviewer: accounts[2]._account_id, state: ReviewerState.REVIEWER},
+ {reviewer: groups[0].id, state: ReviewerState.REVIEWER},
{reviewer: accounts[5]._account_id, state: ReviewerState.CC},
],
+ ignore_automatic_attention_set_rules: true,
+ // only the reviewer is added to the attention set, not the cc
+ add_to_attention_set: [
+ {
+ reason: '<GERRIT_ACCOUNT_1> replied on the change',
+ user: accounts[2]._account_id,
+ },
+ {
+ reason: '<GERRIT_ACCOUNT_1> replied on the change',
+ user: groups[0].id,
+ },
+ ],
},
]);
assert.sameDeepOrderedMembers(saveChangeReviewStub.secondCall.args, [
@@ -253,8 +268,21 @@
{
reviewers: [
{reviewer: accounts[2]._account_id, state: ReviewerState.REVIEWER},
+ {reviewer: groups[0].id, state: ReviewerState.REVIEWER},
{reviewer: accounts[5]._account_id, state: ReviewerState.CC},
],
+ ignore_automatic_attention_set_rules: true,
+ // only the reviewer is added to the attention set, not the cc
+ add_to_attention_set: [
+ {
+ reason: '<GERRIT_ACCOUNT_1> replied on the change',
+ user: accounts[2]._account_id,
+ },
+ {
+ reason: '<GERRIT_ACCOUNT_1> replied on the change',
+ user: groups[0].id,
+ },
+ ],
},
]);
});
@@ -357,7 +385,7 @@
);
await flush();
- // prettier and shadoDom string don't agree on long text in divs
+ // prettier and shadowDom string don't agree on long text in divs
expect(element).shadowDom.to.equal(
/* prettier-ignore */
/* HTML */ `
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 2a90bfc..2f5965b 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
@@ -45,7 +45,6 @@
import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {queryAll} from '../../../utils/common-util';
import {ValueChangedEvent} from '../../../types/events';
-import {KnownExperimentId} from '../../../services/flags/flags';
import {GrChangeListSection} from '../gr-change-list-section/gr-change-list-section';
import {Execution} from '../../../constants/reporting';
@@ -309,11 +308,7 @@
const prefColumns = this.preferences.change_table
.map(column => (column === 'Project' ? ColumnNames.REPO : column))
.map(column =>
- this.flagsService.isEnabled(
- KnownExperimentId.SUBMIT_REQUIREMENTS_UI
- ) && column === ColumnNames.STATUS
- ? ColumnNames.STATUS2
- : column
+ column === ColumnNames.STATUS ? ColumnNames.STATUS2 : column
);
this.reporting.reportExecution(Execution.USER_PREFERENCES_COLUMNS, {
statusColumn: prefColumns.includes(ColumnNames.STATUS2),
@@ -336,50 +331,23 @@
if (!config || !config.change) return true;
if (column === 'Comments')
return this.flagsService.isEnabled('comments-column');
- if (column === 'Status') {
- return !this.flagsService.isEnabled(
- KnownExperimentId.SUBMIT_REQUIREMENTS_UI
- );
- }
- if (column === ColumnNames.STATUS2)
- return this.flagsService.isEnabled(
- KnownExperimentId.SUBMIT_REQUIREMENTS_UI
- );
+ if (column === 'Status') return false;
+ if (column === ColumnNames.STATUS2) return true;
return true;
}
// private but used in test
computeLabelNames(sections: ChangeListSection[]) {
if (!sections) return [];
- let labels: string[] = [];
- const nonExistingLabel = function (item: string) {
- return !labels.includes(item);
- };
- for (const section of sections) {
- if (!section.results) {
- continue;
- }
- for (const change of section.results) {
- if (!change.labels) {
- continue;
- }
- const currentLabels = Object.keys(change.labels);
- labels = labels.concat(currentLabels.filter(nonExistingLabel));
- }
+ if (this.config?.submit_requirement_dashboard_columns?.length) {
+ return this.config?.submit_requirement_dashboard_columns;
}
-
- if (this.flagsService.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI)) {
- if (this.config?.submit_requirement_dashboard_columns?.length) {
- return this.config?.submit_requirement_dashboard_columns;
- } else {
- const changes = sections.map(section => section.results).flat();
- labels = (changes ?? [])
- .map(change => getRequirements(change))
- .flat()
- .map(requirement => requirement.name)
- .filter(unique);
- }
- }
+ const changes = sections.map(section => section.results).flat();
+ const labels = (changes ?? [])
+ .map(change => getRequirements(change))
+ .flat()
+ .map(requirement => requirement.name)
+ .filter(unique);
return labels.sort();
}
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
index 63a6d8f..1e81123 100644
--- 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
@@ -32,6 +32,7 @@
import {
createChange,
createServerInfo,
+ createSubmitRequirementResultInfo,
} from '../../../test/test-data-generators';
import {GrChangeListItem} from '../gr-change-list-item/gr-change-list-item';
import {GrChangeListSection} from '../gr-change-list-section/gr-change-list-section';
@@ -165,23 +166,36 @@
{
...createChange(),
_number: 0 as NumericChangeId,
- labels: {Verified: {approved: {}}},
+ submit_requirements: [
+ {
+ ...createSubmitRequirementResultInfo(),
+ name: 'Verified',
+ },
+ ],
},
{
...createChange(),
_number: 1 as NumericChangeId,
- labels: {
- Verified: {approved: {}},
- 'Code-Review': {approved: {}},
- },
+ submit_requirements: [
+ {
+ ...createSubmitRequirementResultInfo(),
+ name: 'Verified',
+ },
+ {
+ ...createSubmitRequirementResultInfo(),
+ name: 'Code-Review',
+ },
+ ],
},
{
...createChange(),
_number: 2 as NumericChangeId,
- labels: {
- Verified: {approved: {}},
- 'Library-Compliance': {approved: {}},
- },
+ submit_requirements: [
+ {
+ ...createSubmitRequirementResultInfo(),
+ name: 'Library-Compliance',
+ },
+ ],
},
],
},
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 95ad376..9df22f1 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
@@ -29,7 +29,6 @@
import '../../shared/gr-linked-chip/gr-linked-chip';
import '../../shared/gr-tooltip-content/gr-tooltip-content';
import '../gr-submit-requirements/gr-submit-requirements';
-import '../gr-change-requirements/gr-change-requirements';
import '../gr-commit-info/gr-commit-info';
import '../gr-reviewer-list/gr-reviewer-list';
import '../../shared/gr-account-list/gr-account-list';
@@ -83,11 +82,7 @@
} from '../../shared/gr-autocomplete/gr-autocomplete';
import {getRevertCreatedChangeIds} from '../../../utils/message-util';
import {Interaction} from '../../../constants/reporting';
-import {
- getApprovalInfo,
- getCodeReviewLabel,
- showNewSubmitRequirements,
-} from '../../../utils/label-util';
+import {getApprovalInfo, getCodeReviewLabel} from '../../../utils/label-util';
import {LitElement, css, html, nothing, PropertyValues} from 'lit';
import {customElement, property, query, state} from 'lit/decorators';
import {sharedStyles} from '../../../styles/shared-styles';
@@ -179,8 +174,6 @@
private readonly reporting = getAppContext().reportingService;
- private readonly flagsService = getAppContext().flagsService;
-
constructor() {
super();
this.queryTopic = (input: string) => this.getTopicSuggestions(input);
@@ -195,7 +188,6 @@
:host {
display: table;
}
- gr-change-requirements,
gr-submit-requirements {
--requirements-horizontal-padding: var(--metadata-horizontal-padding);
}
@@ -702,23 +694,13 @@
}
private renderSubmitRequirements() {
- if (this.showNewSubmitRequirements()) {
- return html`<div class="separatedSection">
- <gr-submit-requirements
- .change=${this.change}
- .account=${this.account}
- .mutable=${this.mutable}
- ></gr-submit-requirements>
- </div>`;
- } else {
- return html` <div class="oldSeparatedSection">
- <gr-change-requirements
- .change=${this.change}
- .account=${this.account}
- .mutable=${this.mutable}
- ></gr-change-requirements>
- </div>`;
- }
+ return html`<div class="separatedSection">
+ <gr-submit-requirements
+ .change=${this.change}
+ .account=${this.account}
+ .mutable=${this.mutable}
+ ></gr-submit-requirements>
+ </div>`;
}
private renderWeblinks() {
@@ -1213,10 +1195,6 @@
);
}
- private showNewSubmitRequirements() {
- return showNewSubmitRequirements(this.flagsService, this.change);
- }
-
private computeVoteForRole(role: ChangeRole) {
const reviewer = this.getNonOwnerRole(role);
if (reviewer && isAccount(reviewer)) {
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 2b48697..a383cb1 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
@@ -199,8 +199,8 @@
<span class="title"> Hashtags </span>
<span class="value"> </span>
</section>
- <div class="oldSeparatedSection">
- <gr-change-requirements></gr-change-requirements>
+ <div class="separatedSection">
+ <gr-submit-requirements></gr-submit-requirements>
</div>
<gr-endpoint-decorator name="change-metadata-item">
<gr-endpoint-param name="labels"> </gr-endpoint-param>
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
deleted file mode 100644
index 821e1ce..0000000
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.ts
+++ /dev/null
@@ -1,208 +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 '../../../styles/shared-styles';
-import '../../../styles/gr-font-styles';
-import '../../shared/gr-button/gr-button';
-import '../../shared/gr-icons/gr-icons';
-import '../../shared/gr-label-info/gr-label-info';
-import '../../shared/gr-limited-text/gr-limited-text';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-change-requirements_html';
-import {customElement, property, observe} from '@polymer/decorators';
-import {
- ChangeInfo,
- AccountInfo,
- QuickLabelInfo,
- Requirement,
- RequirementType,
- LabelNameToInfoMap,
- LabelInfo,
-} from '../../../types/common';
-import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
-import {getAppContext} from '../../../services/app-context';
-import {labelCompare} from '../../../utils/label-util';
-import {Interaction} from '../../../constants/reporting';
-
-interface ChangeRequirement extends Requirement {
- satisfied: boolean;
- style: string;
-}
-
-interface ChangeWIP {
- type: RequirementType;
- fallback_text: string;
- tooltip: string;
-}
-
-export interface Label {
- labelName: string;
- labelInfo: LabelInfo;
- icon: string;
- style: string;
-}
-
-@customElement('gr-change-requirements')
-export class GrChangeRequirements extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
- @property({type: Object})
- change?: ChangeInfo;
-
- @property({type: Object})
- account?: AccountInfo;
-
- @property({type: Boolean})
- mutable?: boolean;
-
- @property({type: Array, computed: '_computeRequirements(change)'})
- _requirements?: Array<ChangeRequirement | ChangeWIP>;
-
- @property({type: Array})
- _requiredLabels: Label[] = [];
-
- @property({type: Array})
- _optionalLabels: Label[] = [];
-
- @property({type: Boolean, computed: '_computeShowWip(change)'})
- _showWip?: boolean;
-
- @property({type: Boolean})
- _showOptionalLabels = true;
-
- private readonly reporting = getAppContext().reportingService;
-
- _computeShowWip(change: ChangeInfo) {
- return change.work_in_progress;
- }
-
- _computeRequirements(change: ChangeInfo) {
- const _requirements: Array<ChangeRequirement | ChangeWIP> = [];
-
- if (change.requirements) {
- for (const requirement of change.requirements) {
- const satisfied = requirement.status === 'OK';
- const style = this._computeRequirementClass(satisfied);
- _requirements.push({...requirement, satisfied, style});
- }
- }
- if (change.work_in_progress) {
- _requirements.push({
- type: 'wip' as RequirementType,
- fallback_text: 'Work-in-progress',
- tooltip: "Change must not be in 'Work in Progress' state.",
- });
- }
-
- return _requirements;
- }
-
- _computeRequirementClass(requirementStatus: boolean) {
- return requirementStatus ? 'approved' : '';
- }
-
- _computeRequirementIcon(requirementStatus: boolean) {
- return requirementStatus ? 'gr-icons:check' : 'gr-icons:schedule';
- }
-
- @observe('change.labels.*')
- _computeLabels(
- labelsRecord: PolymerDeepPropertyChange<
- LabelNameToInfoMap,
- LabelNameToInfoMap
- >
- ) {
- const labels = labelsRecord.base || {};
- const allLabels: Label[] = [];
-
- for (const label of Object.keys(labels).sort(labelCompare)) {
- allLabels.push({
- labelName: label,
- icon: this._computeLabelIcon(labels[label]),
- style: this._computeLabelClass(labels[label]),
- labelInfo: labels[label],
- });
- }
- this._optionalLabels = allLabels.filter(label => label.labelInfo.optional);
- this._requiredLabels = allLabels.filter(label => !label.labelInfo.optional);
- }
-
- /**
- * @return The icon name, or undefined if no icon should
- * be used.
- */
- _computeLabelIcon(labelInfo: QuickLabelInfo) {
- if (labelInfo.approved) {
- return 'gr-icons:check';
- }
- if (labelInfo.rejected) {
- return 'gr-icons:close';
- }
- return 'gr-icons:schedule';
- }
-
- _computeLabelClass(labelInfo: QuickLabelInfo) {
- if (labelInfo.approved) {
- return 'approved';
- }
- if (labelInfo.rejected) {
- return 'rejected';
- }
- return '';
- }
-
- _computeShowOptional(
- optionalFieldsRecord: PolymerDeepPropertyChange<Label[], Label[]>
- ) {
- return optionalFieldsRecord.base.length ? '' : 'hidden';
- }
-
- _computeLabelValue(value: number) {
- return `${value > 0 ? '+' : ''}${value}`;
- }
-
- _computeSectionClass(show: boolean) {
- return show ? '' : 'hidden';
- }
-
- _handleShowHide() {
- this._showOptionalLabels = !this._showOptionalLabels;
- this.reporting.reportInteraction(Interaction.TOGGLE_SHOW_ALL_BUTTON, {
- sectionName: 'optional labels',
- toState: this._showOptionalLabels ? 'Show all' : 'Show less',
- });
- }
-
- _computeSubmitRequirementEndpoint(item: ChangeRequirement | ChangeWIP) {
- return `submit-requirement-item-${item.type}`;
- }
-
- _computeShowAllLabelText(_showOptionalLabels: boolean) {
- if (_showOptionalLabels) {
- return 'Show less';
- } else {
- return 'Show all';
- }
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-change-requirements': GrChangeRequirements;
- }
-}
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
deleted file mode 100644
index 0005c90..0000000
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts
+++ /dev/null
@@ -1,211 +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: table;
- width: 100%;
- }
- .status {
- color: var(--warning-foreground);
- display: inline-block;
- text-align: center;
- vertical-align: top;
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-mono);
- line-height: var(--line-height-mono);
- }
- .approved.status {
- color: var(--positive-green-text-color);
- }
- .rejected.status {
- color: var(--negative-red-text-color);
- }
- iron-icon {
- color: inherit;
- }
- .status iron-icon {
- vertical-align: top;
- }
- gr-endpoint-decorator.submit-requirement-endpoints,
- section {
- display: table-row;
- }
- .show-hide {
- float: right;
- }
- .title {
- min-width: 10em;
- padding: var(--spacing-s) var(--spacing-m) 0
- var(--requirements-horizontal-padding);
- }
- .value {
- padding: var(--spacing-s) 0 0 0;
- }
- .title,
- .value {
- display: table-cell;
- vertical-align: top;
- }
- .hidden {
- display: none;
- }
- .showHide {
- cursor: pointer;
- }
- .showHide .title {
- padding-bottom: var(--spacing-m);
- padding-top: var(--spacing-l);
- }
- .showHide .value {
- padding-top: 0;
- vertical-align: middle;
- }
- .showHide iron-icon {
- color: var(--deemphasized-text-color);
- float: right;
- }
- .show-all-button {
- float: right;
- }
- .show-all-button iron-icon {
- color: inherit;
- --iron-icon-height: 18px;
- --iron-icon-width: 18px;
- }
- .spacer {
- height: var(--spacing-m);
- }
- gr-endpoint-param {
- display: none;
- }
- .metadata-title {
- font-weight: var(--font-weight-bold);
- color: var(--deemphasized-text-color);
- padding-left: var(--metadata-horizontal-padding);
- }
- .title .metadata-title {
- padding-left: 0;
- }
- </style>
- <h3 class="metadata-title heading-3">Submit requirements</h3>
- <template is="dom-repeat" items="[[_requirements]]">
- <gr-endpoint-decorator
- class="submit-requirement-endpoints"
- name$="[[_computeSubmitRequirementEndpoint(item)]]"
- >
- <gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
- <gr-endpoint-param name="requirement" value="[[item]]">
- </gr-endpoint-param>
- <div class="title requirement">
- <span class$="status [[item.style]]">
- <iron-icon
- class="icon"
- icon="[[_computeRequirementIcon(item.satisfied)]]"
- ></iron-icon>
- </span>
- <gr-limited-text
- class="name"
- tooltip="[[item.tooltip]]"
- text="[[item.fallback_text]]"
- ></gr-limited-text>
- </div>
- <div class="value">
- <gr-endpoint-slot name="value"></gr-endpoint-slot>
- </div>
- </gr-endpoint-decorator>
- </template>
- <template is="dom-repeat" items="[[_requiredLabels]]">
- <section>
- <div class="title">
- <span class$="status [[item.style]]">
- <iron-icon class="icon" icon="[[item.icon]]"></iron-icon>
- </span>
- <gr-limited-text
- class="name"
- text="[[item.labelName]]"
- ></gr-limited-text>
- </div>
- <div class="value">
- <gr-label-info
- change="{{change}}"
- account="[[account]]"
- mutable="[[mutable]]"
- label="[[item.labelName]]"
- label-info="[[item.labelInfo]]"
- ></gr-label-info>
- </div>
- </section>
- </template>
- <section class="spacer"></section>
- <section
- class$="spacer [[_computeShowOptional(_optionalLabels.*)]]"
- ></section>
- <section class$="showHide [[_computeShowOptional(_optionalLabels.*)]]">
- <div class="title">
- <h3 class="metadata-title">Other labels</h3>
- </div>
- <div class="value">
- <gr-button link="" class="show-all-button" on-click="_handleShowHide"
- >[[_computeShowAllLabelText(_showOptionalLabels)]]
- <iron-icon
- icon="gr-icons:expand-more"
- hidden$="[[_showOptionalLabels]]"
- ></iron-icon
- ><iron-icon
- icon="gr-icons:expand-less"
- hidden$="[[!_showOptionalLabels]]"
- ></iron-icon>
- </gr-button>
- </div>
- </section>
- <template is="dom-repeat" items="[[_optionalLabels]]">
- <section class$="optional [[_computeSectionClass(_showOptionalLabels)]]">
- <div class="title">
- <span class$="status [[item.style]]">
- <template is="dom-if" if="[[item.icon]]">
- <iron-icon class="icon" icon="[[item.icon]]"></iron-icon>
- </template>
- <template is="dom-if" if="[[!item.icon]]">
- <span>[[_computeLabelValue(item.labelInfo.value)]]</span>
- </template>
- </span>
- <gr-limited-text
- class="name"
- text="[[item.labelName]]"
- ></gr-limited-text>
- </div>
- <div class="value">
- <gr-label-info
- change="{{change}}"
- account="[[account]]"
- mutable="[[mutable]]"
- label="[[item.labelName]]"
- label-info="[[item.labelInfo]]"
- ></gr-label-info>
- </div>
- </section>
- </template>
- <section
- class$="spacer [[_computeShowOptional(_optionalLabels.*)]] [[_computeSectionClass(_showOptionalLabels)]]"
- ></section>
-`;
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.js b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.js
deleted file mode 100644
index 90f9d29..0000000
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.js
+++ /dev/null
@@ -1,222 +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-change-requirements.js';
-import {isHidden} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-change-requirements');
-
-suite('gr-change-metadata tests', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- });
-
- test('requirements computed fields', () => {
- assert.isTrue(element._computeShowWip({work_in_progress: true}));
- assert.isFalse(element._computeShowWip({work_in_progress: false}));
-
- assert.equal(element._computeRequirementClass(true), 'approved');
- assert.equal(element._computeRequirementClass(false), '');
-
- assert.equal(element._computeRequirementIcon(true), 'gr-icons:check');
- assert.equal(element._computeRequirementIcon(false),
- 'gr-icons:schedule');
- });
-
- test('label computed fields', () => {
- assert.equal(element._computeLabelIcon({approved: []}), 'gr-icons:check');
- assert.equal(element._computeLabelIcon({rejected: []}), 'gr-icons:close');
- assert.equal(element._computeLabelIcon({}), 'gr-icons:schedule');
-
- assert.equal(element._computeLabelClass({approved: []}), 'approved');
- assert.equal(element._computeLabelClass({rejected: []}), 'rejected');
- assert.equal(element._computeLabelClass({}), '');
- assert.equal(element._computeLabelClass({value: 0}), '');
-
- assert.equal(element._computeLabelValue(1), '+1');
- assert.equal(element._computeLabelValue(-1), '-1');
- assert.equal(element._computeLabelValue(0), '0');
- });
-
- test('_computeLabels', () => {
- assert.equal(element._optionalLabels.length, 0);
- assert.equal(element._requiredLabels.length, 0);
- element._computeLabels({base: {
- test: {
- all: [{_account_id: 1, name: 'bojack', value: 1}],
- default_value: 0,
- values: [],
- value: 1,
- },
- opt_test: {
- all: [{_account_id: 1, name: 'bojack', value: 1}],
- default_value: 0,
- values: [],
- optional: true,
- },
- }});
- assert.equal(element._optionalLabels.length, 1);
- assert.equal(element._requiredLabels.length, 1);
-
- assert.equal(element._optionalLabels[0].labelName, 'opt_test');
- assert.equal(element._optionalLabels[0].icon, 'gr-icons:schedule');
- assert.equal(element._optionalLabels[0].style, '');
- assert.ok(element._optionalLabels[0].labelInfo);
- });
-
- test('optional show/hide', () => {
- element._optionalLabels = [{label: 'test'}];
- flush();
-
- assert.ok(element.shadowRoot
- .querySelector('section.optional'));
- MockInteractions.tap(element.shadowRoot
- .querySelector('.show-all-button'));
- flush();
-
- assert.isFalse(element._showOptionalLabels);
- assert.isTrue(isHidden(element.shadowRoot
- .querySelector('section.optional')));
- });
-
- test('properly converts satisfied labels', () => {
- element.change = {
- status: 'NEW',
- labels: {
- Verified: {
- approved: [],
- },
- },
- requirements: [],
- };
- flush();
-
- assert.ok(element.shadowRoot
- .querySelector('.approved'));
- assert.ok(element.shadowRoot
- .querySelector('.name'));
- assert.equal(element.shadowRoot
- .querySelector('.name').text, 'Verified');
- });
-
- test('properly converts unsatisfied labels', () => {
- element.change = {
- status: 'NEW',
- labels: {
- Verified: {
- approved: false,
- },
- },
- };
- flush();
-
- const name = element.shadowRoot
- .querySelector('.name');
- assert.ok(name);
- assert.isFalse(name.hasAttribute('hidden'));
- assert.equal(name.text, 'Verified');
- });
-
- test('properly displays Work In Progress', () => {
- element.change = {
- status: 'NEW',
- labels: {},
- requirements: [],
- work_in_progress: true,
- };
- flush();
-
- const changeIsWip = element.shadowRoot
- .querySelector('.title');
- assert.ok(changeIsWip);
- });
-
- test('properly displays a satisfied requirement', () => {
- element.change = {
- status: 'NEW',
- labels: {},
- requirements: [{
- fallback_text: 'Resolve all comments',
- status: 'OK',
- }],
- };
- flush();
-
- const requirement = element.shadowRoot
- .querySelector('.requirement');
- assert.ok(requirement);
- assert.isFalse(requirement.hasAttribute('hidden'));
- assert.ok(requirement.querySelector('.approved'));
- assert.equal(requirement.querySelector('.name').text,
- 'Resolve all comments');
- });
-
- test('satisfied class is applied with OK', () => {
- element.change = {
- status: 'NEW',
- labels: {},
- requirements: [{
- fallback_text: 'Resolve all comments',
- status: 'OK',
- }],
- };
- flush();
-
- const requirement = element.shadowRoot
- .querySelector('.requirement');
- assert.ok(requirement);
- assert.ok(requirement.querySelector('.approved'));
- });
-
- test('satisfied class is not applied with NOT_READY', () => {
- element.change = {
- status: 'NEW',
- labels: {},
- requirements: [{
- fallback_text: 'Resolve all comments',
- status: 'NOT_READY',
- }],
- };
- flush();
-
- const requirement = element.shadowRoot
- .querySelector('.requirement');
- assert.ok(requirement);
- assert.strictEqual(requirement.querySelector('.approved'), null);
- });
-
- test('satisfied class is not applied with RULE_ERROR', () => {
- element.change = {
- status: 'NEW',
- labels: {},
- requirements: [{
- fallback_text: 'Resolve all comments',
- status: 'RULE_ERROR',
- }],
- };
- flush();
-
- const requirement = element.shadowRoot
- .querySelector('.requirement');
- assert.ok(requirement);
- assert.strictEqual(requirement.querySelector('.approved'), null);
- });
-});
-
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 6ec815c..0d52e79 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
@@ -821,30 +821,6 @@
return html`
<div>
<table>
- <tr ?hidden=${!this.showChecksSummary}>
- <td class="key">Checks</td>
- <td class="value">
- <div class="checksSummary">
- ${this.renderChecksZeroState()}${this.renderChecksChipForCategory(
- Category.ERROR
- )}${this.renderChecksChipForCategory(
- Category.WARNING
- )}${this.renderChecksChipForCategory(
- Category.INFO
- )}${this.renderChecksChipForCategory(
- Category.SUCCESS
- )}${hasNonRunningChip && hasRunningChip
- ? html`<br />`
- : ''}${this.renderChecksChipRunning()}
- <span
- class="loadingSpin"
- ?hidden=${!this.someProvidersAreLoading}
- ></span>
- ${this.renderErrorMessages()} ${this.renderChecksLogin()}
- ${this.renderSummaryMessage()} ${this.renderActions()}
- </div>
- </td>
- </tr>
<tr>
<td class="key">Comments</td>
<td class="value">
@@ -884,6 +860,30 @@
>
</td>
</tr>
+ <tr ?hidden=${!this.showChecksSummary}>
+ <td class="key">Checks</td>
+ <td class="value">
+ <div class="checksSummary">
+ ${this.renderChecksZeroState()}${this.renderChecksChipForCategory(
+ Category.ERROR
+ )}${this.renderChecksChipForCategory(
+ Category.WARNING
+ )}${this.renderChecksChipForCategory(
+ Category.INFO
+ )}${this.renderChecksChipForCategory(
+ Category.SUCCESS
+ )}${hasNonRunningChip && hasRunningChip
+ ? html`<br />`
+ : ''}${this.renderChecksChipRunning()}
+ <span
+ class="loadingSpin"
+ ?hidden=${!this.someProvidersAreLoading}
+ ></span>
+ ${this.renderErrorMessages()} ${this.renderChecksLogin()}
+ ${this.renderSummaryMessage()} ${this.renderActions()}
+ </div>
+ </td>
+ </tr>
<tr hidden>
<td class="key">Findings</td>
<td class="value"></td>
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 7e4cf2c..3db1012 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
@@ -633,6 +633,12 @@
private connected$ = new BehaviorSubject(false);
+ /**
+ * For `connectedCallback()` to distinguish between connecting to the DOM for
+ * the first time or if just re-connecting.
+ */
+ private isFirstConnection = true;
+
/** Simply reflects the router-model value. */
// visible for testing
routerPatchNum?: PatchSetNum;
@@ -649,12 +655,29 @@
'fullscreen-overlay-opened',
() => this._handleHideBackgroundContent()
);
-
this.addEventListener('fullscreen-overlay-closed', () =>
this._handleShowBackgroundContent()
);
-
this.addEventListener('open-reply-dialog', () => this._openReplyDialog());
+ this.addEventListener('change-message-deleted', () => fireReload(this));
+ this.addEventListener('editable-content-save', e =>
+ this._handleCommitMessageSave(e)
+ );
+ this.addEventListener('editable-content-cancel', () =>
+ this._handleCommitMessageCancel()
+ );
+ this.addEventListener('open-fix-preview', e => this._onOpenFixPreview(e));
+ this.addEventListener('close-fix-preview', e => this._onCloseFixPreview(e));
+
+ this.addEventListener(EventType.SHOW_PRIMARY_TAB, e =>
+ this._setActivePrimaryTab(e)
+ );
+ this.addEventListener('reload', e => {
+ this.loadData(
+ /* isLocationChange= */ false,
+ /* clearPatchset= */ e.detail && e.detail.clearPatchset
+ );
+ });
}
private setupSubscriptions() {
@@ -699,24 +722,23 @@
override connectedCallback() {
super.connectedCallback();
+ this.firstConnectedCallback();
this.connected$.next(true);
- this.setupSubscriptions();
- this._throttledToggleChangeStar = throttleWrap<KeyboardEvent>(_ =>
- this._handleToggleChangeStar()
- );
- this._getServerConfig().then(config => {
- this._serverConfig = config;
- this._replyDisabled = false;
- });
- this._getLoggedIn().then(loggedIn => {
- this._loggedIn = loggedIn;
- if (loggedIn) {
- this.restApiService.getAccount().then(acct => {
- this._account = acct;
- });
- }
- });
+ // Make sure to reverse everything below this line in disconnectedCallback().
+ // Or consider using either firstConnectedCallback() or constructor().
+ this.setupSubscriptions();
+ document.addEventListener('visibilitychange', this.handleVisibilityChange);
+ document.addEventListener('scroll', this.handleScroll);
+ }
+
+ /**
+ * For initialization that should only happen once, not again when
+ * re-connecting to the DOM later.
+ */
+ private firstConnectedCallback() {
+ if (!this.isFirstConnection) return;
+ this.isFirstConnection = false;
getPluginLoader()
.awaitPluginsLoaded()
@@ -734,26 +756,21 @@
})
.then(() => this._initActiveTabs(this.params));
- this.addEventListener('change-message-deleted', () => fireReload(this));
- this.addEventListener('editable-content-save', e =>
- this._handleCommitMessageSave(e)
+ this._throttledToggleChangeStar = throttleWrap<KeyboardEvent>(_ =>
+ this._handleToggleChangeStar()
);
- this.addEventListener('editable-content-cancel', () =>
- this._handleCommitMessageCancel()
- );
- 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._getServerConfig().then(config => {
+ this._serverConfig = config;
+ this._replyDisabled = false;
+ });
- this.addEventListener(EventType.SHOW_PRIMARY_TAB, e =>
- this._setActivePrimaryTab(e)
- );
- this.addEventListener('reload', e => {
- this.loadData(
- /* isLocationChange= */ false,
- /* clearPatchset= */ e.detail && e.detail.clearPatchset
- );
+ this._getLoggedIn().then(loggedIn => {
+ this._loggedIn = loggedIn;
+ if (loggedIn) {
+ this.restApiService.getAccount().then(acct => {
+ this._account = acct;
+ });
+ }
});
}
@@ -944,6 +961,8 @@
assertIsDefined(this._change, '_change');
if (!this._changeNum)
throw new Error('missing required changeNum property');
+ // to prevent 2 requests at the same time
+ if (this.$.commitMessageEditor.disabled) return;
// Trim trailing whitespace from each line.
const message = e.detail.content.replace(TRAILING_WHITESPACE_REGEX, '');
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 5d4db56..0eac350 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
@@ -546,13 +546,13 @@
diff-prefs="{{_diffPrefs}}"
change="[[_change]]"
change-num="[[_changeNum]]"
- patch-range="{{_patchRange}}"
+ patch-range="[[_patchRange]]"
selected-index="{{viewState.selectedFileIndex}}"
diff-view-mode="[[viewState.diffMode]]"
edit-mode="[[_editMode]]"
num-files-shown="{{_numFilesShown}}"
files-expanded="{{_filesExpanded}}"
- file-list-increment="{{_numFilesShown}}"
+ file-list-increment="[[_numFilesShown]]"
on-files-shown-changed="_setShownFiles"
on-file-action-tap="_handleFileActionTap"
observer-target="[[_computeObserverTarget()]]"
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 80cf8a4..ce45424 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
@@ -103,6 +103,7 @@
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {GrChangeStar} from '../../shared/gr-change-star/gr-change-star';
import {GrThreadList} from '../gr-thread-list/gr-thread-list';
+import {assertIsDefined} from '../../../utils/common-util';
const fixture = fixtureFromElement('gr-change-view');
@@ -782,11 +783,10 @@
assert.isTrue(stub.called);
});
- test(', should open diff preferences', () => {
- const stub = sinon.stub(
- element.$.fileList.$.diffPreferencesDialog,
- 'open'
- );
+ test(', should open diff preferences', async () => {
+ await element.$.fileList.updateComplete;
+ assertIsDefined(element.$.fileList.diffPreferencesDialog);
+ const stub = sinon.stub(element.$.fileList.diffPreferencesDialog, 'open');
element._loggedIn = false;
pressAndReleaseKeyOn(element, 188, null, ',');
assert.isFalse(stub.called);
@@ -1183,7 +1183,8 @@
});
});
- test('diff preferences open when open-diff-prefs is fired', () => {
+ test('diff preferences open when open-diff-prefs is fired', async () => {
+ await element.$.fileList.updateComplete;
const overlayOpenStub = sinon.stub(element.$.fileList, 'openDiffPrefs');
element.$.fileListHeader.dispatchEvent(
new CustomEvent('open-diff-prefs', {
@@ -1326,6 +1327,7 @@
.stub(element, '_reloadPatchNumDependentResources')
.callsFake(() => Promise.resolve([undefined, undefined, undefined]));
flush();
+ await element.$.fileList.updateComplete;
const collapseStub = sinon.stub(element.$.fileList, 'collapseAllDiffs');
const value: AppElementChangeViewParams = {
...createAppElementChangeViewParams(),
@@ -1359,7 +1361,7 @@
sinon.stub(element, 'loadData').callsFake(() => Promise.resolve());
sinon.stub(element, 'loadAndSetCommitInfo');
sinon.stub(element.$.fileList, 'reload');
- flush();
+ await flush();
const reloadPortedCommentsStub = sinon.stub(
element.getCommentsModel(),
'reloadPortedComments'
@@ -1529,7 +1531,7 @@
);
});
- test('_handleCommitMessageSave trims trailing whitespace', () => {
+ test('_handleCommitMessageSave trims trailing whitespace', async () => {
element._change = createChangeViewChange();
// Response code is 500, because we want to avoid window reloading
const putStub = stubRestApi('putChangeCommitMessage').returns(
@@ -1541,10 +1543,10 @@
element._handleCommitMessageSave(mockEvent('test \n test '));
assert.equal(putStub.lastCall.args[1], 'test\n test');
-
+ element.$.commitMessageEditor.disabled = false;
element._handleCommitMessageSave(mockEvent(' test\ntest'));
assert.equal(putStub.lastCall.args[1], ' test\ntest');
-
+ element.$.commitMessageEditor.disabled = false;
element._handleCommitMessageSave(mockEvent('\n\n\n\n\n\n\n\n'));
assert.equal(putStub.lastCall.args[1], '\n\n\n\n\n\n\n\n');
});
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 7625756..20d625c 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,7 +14,6 @@
* 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 '../../../embed/diff/gr-diff-cursor/gr-diff-cursor';
@@ -29,14 +28,7 @@
import '../../shared/gr-tooltip-content/gr-tooltip-content';
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 {htmlTemplate} from './gr-file-list_html';
import {asyncForeach, debounce, DelayedTask} from '../../../utils/async-util';
-import {
- KeyboardShortcutMixin,
- Shortcut,
- ShortcutListener,
-} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
import {FilesExpandedState} from '../gr-file-list-constants';
import {pluralize} from '../../../utils/string-util';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
@@ -62,33 +54,40 @@
isMagicPath,
specialFilePathCompare,
} from '../../../utils/path-list-util';
-import {customElement, observe, property} from '@polymer/decorators';
+import {customElement, property, query, state} from 'lit/decorators';
import {
BasePatchSetNum,
EditPatchSetNum,
- ElementPropertyDeepChange,
FileInfo,
FileNameToFileInfoMap,
NumericChangeId,
PatchRange,
- RevisionPatchSetNum,
} from '../../../types/common';
import {DiffPreferencesInfo} from '../../../types/diff';
import {GrDiffHost} from '../../diff/gr-diff-host/gr-diff-host';
import {GrDiffPreferencesDialog} from '../../diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog';
import {GrDiffCursor} from '../../../embed/diff/gr-diff-cursor/gr-diff-cursor';
import {GrCursorManager} from '../../shared/gr-cursor-manager/gr-cursor-manager';
-import {PolymerSpliceChange} from '@polymer/polymer/interfaces';
import {ChangeComments} from '../../diff/gr-comment-api/gr-comment-api';
import {ParsedChangeInfo, PatchSetFile} from '../../../types/types';
import {Timing} from '../../../constants/reporting';
import {RevisionInfo} from '../../shared/revision-info/revision-info';
-import {listen} from '../../../services/shortcuts/shortcuts-service';
import {select} from '../../../utils/observable-util';
-import {resolve, DIPolymerElement} from '../../../models/dependency';
+import {resolve} from '../../../models/dependency';
import {browserModelToken} from '../../../models/browser/browser-model';
import {commentsModelToken} from '../../../models/comments/comments-model';
import {changeModelToken} from '../../../models/change/change-model';
+import {ShortcutController} from '../../lit/shortcut-controller';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
+import {Shortcut} from '../../../services/shortcuts/shortcuts-config';
+import {fire} from '../../../utils/event-util';
+import {a11yStyles} from '../../../styles/gr-a11y-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {ValueChangedEvent} from '../../../types/events';
+import {subscribe} from '../../lit/subscription-controller';
+import {when} from 'lit/directives/when';
+import {incrementalRepeat} from '../../lit/incremental-repeat';
+import {ifDefined} from 'lit/directives/if-defined';
export const DEFAULT_NUM_FILES_SHOWN = 200;
@@ -101,12 +100,6 @@
const FILE_ROW_CLASS = 'file-row';
-export interface GrFileList {
- $: {
- diffPreferencesDialog: GrDiffPreferencesDialog;
- };
-}
-
interface ReviewedFileInfo extends FileInfo {
isReviewed?: boolean;
}
@@ -177,14 +170,26 @@
* @property {number} lines_inserted - fallback to 0 if not present in api
*/
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = KeyboardShortcutMixin(DIPolymerElement);
-
-@customElement('gr-file-list')
-export class GrFileList extends base {
- static get template() {
- return htmlTemplate;
+declare global {
+ interface HTMLElementEventMap {
+ 'num-files-shown-changed': ValueChangedEvent<number>;
+ 'files-expanded-changed': ValueChangedEvent<FilesExpandedState>;
+ 'diff-prefs-changed': ValueChangedEvent<DiffPreferencesInfo>;
}
+ interface HTMLElementTagNameMap {
+ 'gr-file-list': GrFileList;
+ }
+}
+@customElement('gr-file-list')
+export class GrFileList extends LitElement {
+ /**
+ * @event selected-index-changed
+ * @event files-expanded-changed
+ * @event num-files-shown-changed
+ * @event diff-prefs-changed
+ */
+ @query('#diffPreferencesDialog')
+ diffPreferencesDialog?: GrDiffPreferencesDialog;
@property({type: Object})
patchRange?: PatchRange;
@@ -198,121 +203,94 @@
@property({type: Object})
changeComments?: ChangeComments;
- @property({type: Number, notify: true})
+ @property({type: Number, attribute: 'selected-index'})
selectedIndex = -1;
@property({type: Object})
change?: ParsedChangeInfo;
- @property({type: String, notify: true, observer: '_updateDiffPreferences'})
+ @property({type: String})
diffViewMode?: DiffViewMode;
- @property({type: Boolean, observer: '_editModeChanged'})
+ @property({type: Boolean})
editMode?: boolean;
- @property({type: String, notify: true})
+ @property({type: String, attribute: 'files-expanded'})
filesExpanded = FilesExpandedState.NONE;
- @property({type: Object})
- _filesByPath?: FileNameToFileInfoMap;
+ // Private but used in tests.
+ @state()
+ filesByPath?: FileNameToFileInfoMap;
- @property({type: Array, observer: '_filesChanged'})
- _files: NormalizedFileInfo[] = [];
+ // Private but used in tests.
+ @state()
+ files: NormalizedFileInfo[] = [];
- @property({type: Boolean})
- _loggedIn = false;
+ // Private but used in tests.
+ @state()
+ loggedIn = false;
@property({type: Array})
reviewed?: string[] = [];
- @property({type: Object, notify: true, observer: '_updateDiffPreferences'})
+ @property({type: Object, attribute: 'diff-prefs'})
diffPrefs?: DiffPreferencesInfo;
- @property({type: Number, notify: true})
+ @property({type: Number, attribute: 'num-files-shown'})
numFilesShown: number = DEFAULT_NUM_FILES_SHOWN;
- @property({type: Object, computed: '_calculatePatchChange(_files)'})
- _patchChange: PatchChange = createDefaultPatchChange();
-
- @property({type: Number})
+ @property({type: Number, attribute: 'file-list-increment'})
fileListIncrement: number = DEFAULT_NUM_FILES_SHOWN;
- @property({type: Boolean, computed: '_shouldHideChangeTotals(_patchChange)'})
- _hideChangeTotals = true;
+ // Private but used in tests.
+ shownFiles: NormalizedFileInfo[] = [];
- @property({
- type: Boolean,
- computed: '_shouldHideBinaryChangeTotals(_patchChange)',
- })
- _hideBinaryChangeTotals = true;
+ @state()
+ private reportinShownFilesIncrement = 0;
- @property({
- type: Array,
- computed: '_computeFilesShown(numFilesShown, _files)',
- })
- _shownFiles: NormalizedFileInfo[] = [];
+ // Private but used in tests.
+ @state()
+ expandedFiles: PatchSetFile[] = [];
- @property({type: Number})
- _reportinShownFilesIncrement = 0;
+ // Private but used in tests.
+ @state()
+ displayLine?: boolean;
- @property({type: Array})
- _expandedFiles: PatchSetFile[] = [];
+ @state()
+ loading?: boolean;
- @property({type: Boolean})
- _displayLine?: boolean;
-
- @property({type: Boolean, observer: '_loadingChanged'})
- _loading?: boolean;
-
- @property({type: Object, computed: '_computeSizeBarLayout(_shownFiles.*)'})
- _sizeBarLayout: SizeBarLayout = createDefaultSizeBarLayout();
-
- @property({type: Boolean})
- _showSizeBars = true;
+ // Private but used in tests.
+ @state()
+ showSizeBars = true;
// For merge commits vs Auto Merge, an extra file row is shown detailing the
// files that were merged without conflict. These files are also passed to any
// plugins.
- @property({type: Array})
- _cleanlyMergedPaths: string[] = [];
+ @state()
+ private cleanlyMergedPaths: string[] = [];
- @property({type: Array})
- _cleanlyMergedOldPaths: string[] = [];
+ // Private but used in tests.
+ @state()
+ cleanlyMergedOldPaths: string[] = [];
- private _cancelForEachDiff?: () => void;
+ private cancelForEachDiff?: () => void;
loadingTask?: DelayedTask;
- @property({
- type: Boolean,
- computed:
- '_computeShowDynamicColumns(_dynamicHeaderEndpoints, ' +
- '_dynamicContentEndpoints, _dynamicSummaryEndpoints)',
- })
- _showDynamicColumns = false;
+ @state()
+ private dynamicHeaderEndpoints?: string[];
- @property({
- type: Boolean,
- computed:
- '_computeShowPrependedDynamicColumns(' +
- '_dynamicPrependedHeaderEndpoints, _dynamicPrependedContentEndpoints)',
- })
- _showPrependedDynamicColumns = false;
+ @state()
+ private dynamicContentEndpoints?: string[];
- @property({type: Array})
- _dynamicHeaderEndpoints?: string[];
+ @state()
+ private dynamicSummaryEndpoints?: string[];
- @property({type: Array})
- _dynamicContentEndpoints?: string[];
+ @state()
+ private dynamicPrependedHeaderEndpoints?: string[];
- @property({type: Array})
- _dynamicSummaryEndpoints?: string[];
-
- @property({type: Array})
- _dynamicPrependedHeaderEndpoints?: string[];
-
- @property({type: Array})
- _dynamicPrependedContentEndpoints?: string[];
+ @state()
+ private dynamicPrependedContentEndpoints?: string[];
private readonly reporting = getAppContext().reportingService;
@@ -326,48 +304,10 @@
private readonly getBrowserModel = resolve(this, browserModelToken);
- private subscriptions: Subscription[] = [];
-
/** Called in disconnectedCallback. */
private cleanups: (() => void)[] = [];
- override keyboardShortcuts(): ShortcutListener[] {
- return [
- listen(Shortcut.LEFT_PANE, _ => this._handleLeftPane()),
- listen(Shortcut.RIGHT_PANE, _ => this._handleRightPane()),
- listen(Shortcut.TOGGLE_INLINE_DIFF, _ => this._handleToggleInlineDiff()),
- listen(Shortcut.TOGGLE_ALL_INLINE_DIFFS, _ => this._toggleInlineDiffs()),
- listen(Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, _ =>
- toggleClass(this, 'hideComments')
- ),
- listen(Shortcut.CURSOR_NEXT_FILE, e => this._handleCursorNext(e)),
- listen(Shortcut.CURSOR_PREV_FILE, e => this._handleCursorPrev(e)),
- // This is already been taken care of by CURSOR_NEXT_FILE above. The two
- // shortcuts share the same bindings. It depends on whether all files
- // are expanded whether the cursor moves to the next file or line.
- listen(Shortcut.NEXT_LINE, _ => {}), // docOnly
- // This is already been taken care of by CURSOR_PREV_FILE above. The two
- // shortcuts share the same bindings. It depends on whether all files
- // are expanded whether the cursor moves to the previous file or line.
- listen(Shortcut.PREV_LINE, _ => {}), // docOnly
- listen(Shortcut.NEW_COMMENT, _ => this._handleNewComment()),
- listen(Shortcut.OPEN_LAST_FILE, _ =>
- this._openSelectedFile(this._files.length - 1)
- ),
- listen(Shortcut.OPEN_FIRST_FILE, _ => this._openSelectedFile(0)),
- listen(Shortcut.OPEN_FILE, _ => this.handleOpenFile()),
- listen(Shortcut.NEXT_CHUNK, _ => this._handleNextChunk()),
- listen(Shortcut.PREV_CHUNK, _ => this._handlePrevChunk()),
- listen(Shortcut.NEXT_COMMENT_THREAD, _ => this._handleNextComment()),
- listen(Shortcut.PREV_COMMENT_THREAD, _ => this._handlePrevComment()),
- listen(Shortcut.TOGGLE_FILE_REVIEWED, _ =>
- this._handleToggleFileReviewed()
- ),
- listen(Shortcut.TOGGLE_LEFT_PANE, _ => this._handleToggleLeftPane()),
- listen(Shortcut.EXPAND_ALL_COMMENT_THREADS, _ => {}), // docOnly
- listen(Shortcut.COLLAPSE_ALL_COMMENT_THREADS, _ => {}), // docOnly
- ];
- }
+ shortcutsController = new ShortcutController(this);
// private but used in test
fileCursor = new GrCursorManager();
@@ -375,80 +315,506 @@
// private but used in test
diffCursor = new GrDiffCursor();
+ static override get styles() {
+ return [
+ a11yStyles,
+ sharedStyles,
+ css`
+ :host {
+ display: block;
+ }
+ .row {
+ align-items: center;
+ border-top: 1px solid var(--border-color);
+ display: flex;
+ min-height: calc(var(--line-height-normal) + 2 * var(--spacing-s));
+ padding: var(--spacing-xs) var(--spacing-l);
+ }
+ /* The class defines a content visible only to screen readers */
+ .noCommentsScreenReaderText {
+ opacity: 0;
+ max-width: 1px;
+ overflow: hidden;
+ display: none;
+ vertical-align: top;
+ }
+ div[role='gridcell']
+ > div.comments
+ > span:empty
+ + span:empty
+ + span.noCommentsScreenReaderText {
+ /* inline-block instead of block, such that it can control width */
+ display: inline-block;
+ }
+ :host(.loading) .row {
+ opacity: 0.5;
+ }
+ :host(.editMode) .hideOnEdit {
+ display: none;
+ }
+ .showOnEdit {
+ display: none;
+ }
+ :host(.editMode) .showOnEdit {
+ display: initial;
+ }
+ .invisible {
+ visibility: hidden;
+ }
+ .header-row {
+ background-color: var(--background-color-secondary);
+ }
+ .controlRow {
+ align-items: center;
+ display: flex;
+ height: 2.25em;
+ justify-content: center;
+ }
+ .controlRow.invisible,
+ .show-hide.invisible {
+ display: none;
+ }
+ .reviewed,
+ .status {
+ align-items: center;
+ display: inline-flex;
+ }
+ .reviewed {
+ display: inline-block;
+ text-align: left;
+ width: 1.5em;
+ }
+ .file-row {
+ cursor: pointer;
+ }
+ .file-row.expanded {
+ border-bottom: 1px solid var(--border-color);
+ position: -webkit-sticky;
+ position: sticky;
+ top: 0;
+ /* Has to visible above the diff view, and by default has a lower
+ z-index. setting to 1 places it directly above. */
+ z-index: 1;
+ }
+ .file-row:hover {
+ background-color: var(--hover-background-color);
+ }
+ .file-row.selected {
+ background-color: var(--selection-background-color);
+ }
+ .file-row.expanded,
+ .file-row.expanded:hover {
+ background-color: var(--expanded-background-color);
+ }
+ .path {
+ cursor: pointer;
+ flex: 1;
+ /* Wrap it into multiple lines if too long. */
+ white-space: normal;
+ word-break: break-word;
+ }
+ .oldPath {
+ color: var(--deemphasized-text-color);
+ }
+ .header-stats {
+ text-align: center;
+ min-width: 7.5em;
+ }
+ .stats {
+ text-align: right;
+ min-width: 7.5em;
+ }
+ .comments {
+ padding-left: var(--spacing-l);
+ min-width: 7.5em;
+ white-space: nowrap;
+ }
+ .row:not(.header-row) .stats,
+ .total-stats {
+ font-family: var(--monospace-font-family);
+ font-size: var(--font-size-mono);
+ line-height: var(--line-height-mono);
+ display: flex;
+ }
+ .sizeBars {
+ margin-left: var(--spacing-m);
+ min-width: 7em;
+ text-align: center;
+ }
+ .sizeBars.hide {
+ display: none;
+ }
+ .added,
+ .removed {
+ display: inline-block;
+ min-width: 3.5em;
+ }
+ .added {
+ color: var(--positive-green-text-color);
+ }
+ .removed {
+ color: var(--negative-red-text-color);
+ text-align: left;
+ min-width: 4em;
+ padding-left: var(--spacing-s);
+ }
+ .drafts {
+ color: var(--error-foreground);
+ font-weight: var(--font-weight-bold);
+ }
+ .show-hide-icon:focus {
+ outline: none;
+ }
+ .show-hide {
+ margin-left: var(--spacing-s);
+ width: 1.9em;
+ }
+ .fileListButton {
+ margin: var(--spacing-m);
+ }
+ .totalChanges {
+ justify-content: flex-end;
+ text-align: right;
+ }
+ .warning {
+ color: var(--deemphasized-text-color);
+ }
+ input.show-hide {
+ display: none;
+ }
+ label.show-hide {
+ cursor: pointer;
+ display: block;
+ min-width: 2em;
+ }
+ gr-diff {
+ display: block;
+ overflow-x: auto;
+ }
+ .truncatedFileName {
+ display: none;
+ }
+ .mobile {
+ display: none;
+ }
+ .reviewed {
+ margin-left: var(--spacing-xxl);
+ width: 15em;
+ }
+ .reviewedSwitch {
+ color: var(--link-color);
+ opacity: 0;
+ justify-content: flex-end;
+ width: 100%;
+ }
+ .reviewedSwitch:hover {
+ cursor: pointer;
+ opacity: 100;
+ }
+ .showParentButton {
+ line-height: var(--line-height-normal);
+ margin-bottom: calc(var(--spacing-s) * -1);
+ margin-left: var(--spacing-m);
+ margin-top: calc(var(--spacing-s) * -1);
+ }
+ .row:focus {
+ outline: none;
+ }
+ .row:hover .reviewedSwitch,
+ .row:focus-within .reviewedSwitch,
+ .row.expanded .reviewedSwitch {
+ opacity: 100;
+ }
+ .reviewedLabel {
+ color: var(--deemphasized-text-color);
+ margin-right: var(--spacing-l);
+ opacity: 0;
+ }
+ .reviewedLabel.isReviewed {
+ display: initial;
+ opacity: 100;
+ }
+ .editFileControls {
+ width: 7em;
+ }
+ .markReviewed:focus {
+ outline: none;
+ }
+ .markReviewed,
+ .pathLink {
+ display: inline-block;
+ margin: -2px 0;
+ padding: var(--spacing-s) 0;
+ text-decoration: none;
+ }
+ .pathLink:hover span.fullFileName,
+ .pathLink:hover span.truncatedFileName {
+ text-decoration: underline;
+ }
+
+ /** copy on file path **/
+ .pathLink gr-copy-clipboard,
+ .oldPath gr-copy-clipboard {
+ display: inline-block;
+ visibility: hidden;
+ vertical-align: bottom;
+ --gr-button-padding: 0px;
+ }
+ .row:focus-within gr-copy-clipboard,
+ .row:hover gr-copy-clipboard {
+ visibility: visible;
+ }
+
+ @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;
+ }
+ .mobile {
+ display: block;
+ }
+ .row.selected {
+ background-color: var(--view-background-color);
+ }
+ .stats {
+ display: none;
+ }
+ .reviewed,
+ .status {
+ justify-content: flex-start;
+ }
+ .comments {
+ min-width: initial;
+ }
+ .expanded .fullFileName,
+ .truncatedFileName {
+ display: inline;
+ }
+ .expanded .truncatedFileName,
+ .fullFileName {
+ display: none;
+ }
+ }
+ :host(.hideComments) {
+ --gr-comment-thread-display: none;
+ }
+ `,
+ ];
+ }
+
constructor() {
super();
this.fileCursor.scrollMode = ScrollMode.KEEP_VISIBLE;
this.fileCursor.cursorTargetClass = 'selected';
this.fileCursor.focusOnMove = true;
+ this.shortcutsController.addAbstract(Shortcut.LEFT_PANE, _ =>
+ this.handleLeftPane()
+ );
+ this.shortcutsController.addAbstract(Shortcut.RIGHT_PANE, _ =>
+ this.handleRightPane()
+ );
+ this.shortcutsController.addAbstract(Shortcut.TOGGLE_INLINE_DIFF, _ =>
+ this.handleToggleInlineDiff()
+ );
+ this.shortcutsController.addAbstract(Shortcut.TOGGLE_ALL_INLINE_DIFFS, _ =>
+ this.toggleInlineDiffs()
+ );
+ this.shortcutsController.addAbstract(
+ Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS,
+ _ => toggleClass(this, 'hideComments')
+ );
+ this.shortcutsController.addAbstract(Shortcut.CURSOR_NEXT_FILE, e =>
+ this.handleCursorNext(e)
+ );
+ this.shortcutsController.addAbstract(Shortcut.CURSOR_PREV_FILE, e =>
+ this.handleCursorPrev(e)
+ );
+ // This is already been taken care of by CURSOR_NEXT_FILE above. The two
+ // shortcuts share the same bindings. It depends on whether all files
+ // are expanded whether the cursor moves to the next file or line.
+ this.shortcutsController.addAbstract(Shortcut.NEXT_LINE, _ => {}); // docOnly
+ // This is already been taken care of by CURSOR_PREV_FILE above. The two
+ // shortcuts share the same bindings. It depends on whether all files
+ // are expanded whether the cursor moves to the previous file or line.
+ this.shortcutsController.addAbstract(Shortcut.PREV_LINE, _ => {}); // docOnly
+ this.shortcutsController.addAbstract(Shortcut.NEW_COMMENT, _ =>
+ this.handleNewComment()
+ );
+ this.shortcutsController.addAbstract(Shortcut.OPEN_LAST_FILE, _ =>
+ this.openSelectedFile(this.files.length - 1)
+ );
+ this.shortcutsController.addAbstract(Shortcut.OPEN_FIRST_FILE, _ =>
+ this.openSelectedFile(0)
+ );
+ this.shortcutsController.addAbstract(Shortcut.OPEN_FILE, _ =>
+ this.handleOpenFile()
+ );
+ this.shortcutsController.addAbstract(Shortcut.NEXT_CHUNK, _ =>
+ this.handleNextChunk()
+ );
+ this.shortcutsController.addAbstract(Shortcut.PREV_CHUNK, _ =>
+ this.handlePrevChunk()
+ );
+ this.shortcutsController.addAbstract(Shortcut.NEXT_COMMENT_THREAD, _ =>
+ this.handleNextComment()
+ );
+ this.shortcutsController.addAbstract(Shortcut.PREV_COMMENT_THREAD, _ =>
+ this.handlePrevComment()
+ );
+ this.shortcutsController.addAbstract(Shortcut.TOGGLE_FILE_REVIEWED, _ =>
+ this.handleToggleFileReviewed()
+ );
+ this.shortcutsController.addAbstract(Shortcut.TOGGLE_LEFT_PANE, _ =>
+ this.handleToggleLeftPane()
+ );
+ this.shortcutsController.addAbstract(
+ Shortcut.EXPAND_ALL_COMMENT_THREADS,
+ _ => {}
+ ); // docOnly
+ this.shortcutsController.addAbstract(
+ Shortcut.COLLAPSE_ALL_COMMENT_THREADS,
+ _ => {}
+ ); // docOnly
+ subscribe(
+ this,
+ () => this.getCommentsModel().changeComments$,
+ changeComments => {
+ this.changeComments = changeComments;
+ }
+ );
+ subscribe(
+ this,
+ () => this.getBrowserModel().diffViewMode$,
+ diffView => {
+ this.diffViewMode = diffView;
+ }
+ );
+ subscribe(
+ this,
+ () => this.userModel.diffPreferences$,
+ diffPreferences => {
+ this.diffPrefs = diffPreferences;
+ fire(this, 'diff-prefs-changed', {value: this.diffPrefs});
+ }
+ );
+ subscribe(
+ this,
+ () =>
+ select(
+ this.userModel.preferences$,
+ prefs => !!prefs?.size_bar_in_change_table
+ ),
+ sizeBarInChangeTable => {
+ this.showSizeBars = sizeBarInChangeTable;
+ }
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().reviewedFiles$,
+ reviewedFiles => {
+ this.reviewed = reviewedFiles ?? [];
+ }
+ );
+ }
+
+ override willUpdate(changedProperties: PropertyValues): void {
+ if (changedProperties.has('filesByPath')) {
+ this.updateCleanlyMergedPaths();
+ }
+ if (
+ changedProperties.has('diffPrefs') ||
+ changedProperties.has('diffViewMode')
+ ) {
+ this.updateDiffPreferences();
+ }
+ if (
+ changedProperties.has('filesByPath') ||
+ changedProperties.has('changeComments') ||
+ changedProperties.has('patchRange') ||
+ changedProperties.has('reviewed') ||
+ changedProperties.has('loading')
+ ) {
+ changedProperties.set('files', this.files);
+ this.computeFiles();
+ }
+ if (changedProperties.has('loading')) {
+ // Should run after files has been updated.
+ this.loadingChanged();
+ }
+ if (changedProperties.has('files')) {
+ this.filesChanged();
+ }
+ if (
+ changedProperties.has('files') ||
+ changedProperties.has('numFilesShown')
+ ) {
+ this.shownFiles = this.computeFilesShown();
+ }
+ if (changedProperties.has('expandedFiles')) {
+ changedProperties.set('filesExpanded', this.filesExpanded);
+ this.expandedFilesChanged(changedProperties.get('expandedFiles'));
+ }
+ if (changedProperties.has('filesExpanded')) {
+ fire(this, 'files-expanded-changed', {value: this.filesExpanded});
+ }
}
override connectedCallback() {
super.connectedCallback();
- 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.getChangeModel().reviewedFiles$.subscribe(reviewedFiles => {
- this.reviewed = reviewedFiles ?? [];
- }),
- ];
getPluginLoader()
.awaitPluginsLoaded()
.then(() => {
- this._dynamicHeaderEndpoints = getPluginEndpoints().getDynamicEndpoints(
+ this.dynamicHeaderEndpoints = getPluginEndpoints().getDynamicEndpoints(
'change-view-file-list-header'
);
- this._dynamicContentEndpoints =
- getPluginEndpoints().getDynamicEndpoints(
- 'change-view-file-list-content'
- );
- this._dynamicPrependedHeaderEndpoints =
+ this.dynamicContentEndpoints = getPluginEndpoints().getDynamicEndpoints(
+ 'change-view-file-list-content'
+ );
+ this.dynamicPrependedHeaderEndpoints =
getPluginEndpoints().getDynamicEndpoints(
'change-view-file-list-header-prepend'
);
- this._dynamicPrependedContentEndpoints =
+ this.dynamicPrependedContentEndpoints =
getPluginEndpoints().getDynamicEndpoints(
'change-view-file-list-content-prepend'
);
- this._dynamicSummaryEndpoints =
- getPluginEndpoints().getDynamicEndpoints(
- 'change-view-file-list-summary'
- );
+ this.dynamicSummaryEndpoints = getPluginEndpoints().getDynamicEndpoints(
+ 'change-view-file-list-summary'
+ );
if (
- this._dynamicHeaderEndpoints.length !==
- this._dynamicContentEndpoints.length
+ this.dynamicHeaderEndpoints.length !==
+ this.dynamicContentEndpoints.length
) {
this.reporting.error(new Error('dynamic header/content mismatch'));
}
if (
- this._dynamicPrependedHeaderEndpoints.length !==
- this._dynamicPrependedContentEndpoints.length
+ this.dynamicPrependedHeaderEndpoints.length !==
+ this.dynamicPrependedContentEndpoints.length
) {
this.reporting.error(new Error('dynamic header/content mismatch'));
}
if (
- this._dynamicHeaderEndpoints.length !==
- this._dynamicSummaryEndpoints.length
+ this.dynamicHeaderEndpoints.length !==
+ this.dynamicSummaryEndpoints.length
) {
this.reporting.error(new Error('dynamic header/content mismatch'));
}
});
this.cleanups.push(
- addGlobalShortcut({key: Key.ESC}, _ => this._handleEscKey()),
+ addGlobalShortcut({key: Key.ESC}, _ => this.handleEscKey()),
addShortcut(this, {key: Key.ENTER}, _ => this.handleOpenFile(), {
shouldSuppress: true,
})
@@ -456,19 +822,589 @@
}
override disconnectedCallback() {
- for (const s of this.subscriptions) {
- s.unsubscribe();
- }
- this.subscriptions = [];
this.diffCursor.dispose();
this.fileCursor.unsetCursor();
- this._cancelDiffs();
+ this.cancelDiffs();
this.loadingTask?.cancel();
for (const cleanup of this.cleanups) cleanup();
this.cleanups = [];
super.disconnectedCallback();
}
+ override render() {
+ this.classList.toggle('editMode', this.editMode);
+ const patchChange = this.calculatePatchChange();
+ return html`
+ <h3 class="assistive-tech-only">File list</h3>
+ ${this.renderContainer()} ${this.renderChangeTotals(patchChange)}
+ ${this.renderBinaryTotals(patchChange)} ${this.renderControlRow()}
+ <gr-diff-preferences-dialog
+ id="diffPreferencesDialog"
+ @reload-diff-preference=${this.handleReloadingDiffPreference}
+ >
+ </gr-diff-preferences-dialog>
+ `;
+ }
+
+ private renderContainer() {
+ return html`
+ <div
+ id="container"
+ @click=${(e: MouseEvent) => this.handleFileListClick(e)}
+ role="grid"
+ aria-label="Files list"
+ >
+ ${this.renderHeaderRow()} ${this.renderShownFiles()}
+ ${when(this.computeShowNumCleanlyMerged(), () =>
+ this.renderCleanlyMerged()
+ )}
+ </div>
+ `;
+ }
+
+ private renderHeaderRow() {
+ const showPrependedDynamicColumns =
+ this.computeShowPrependedDynamicColumns();
+ const showDynamicColumns = this.computeShowDynamicColumns();
+ return html` <div class="header-row row" role="row">
+ <!-- endpoint: change-view-file-list-header-prepend -->
+ ${when(showPrependedDynamicColumns, () =>
+ this.renderPrependedHeaderEndpoints()
+ )}
+
+ <div class="path" role="columnheader">File</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 -->
+ ${when(showDynamicColumns, () => this.renderDynamicHeaderEndpoints())}
+ <!-- Empty div here exists to keep spacing in sync with file rows. -->
+ <div
+ class="reviewed hideOnEdit"
+ ?hidden=${!this.loggedIn}
+ aria-hidden="true"
+ ></div>
+ <div class="editFileControls showOnEdit" aria-hidden="true"></div>
+ <div class="show-hide" aria-hidden="true"></div>
+ </div>`;
+ }
+
+ private renderPrependedHeaderEndpoints() {
+ return this.dynamicPrependedHeaderEndpoints?.map(
+ headerEndpoint => html`
+ <gr-endpoint-decorator
+ class="prepended-col"
+ .name=${headerEndpoint}
+ role="columnheader"
+ >
+ <gr-endpoint-param name="change" .value=${this.change}>
+ </gr-endpoint-param>
+ <gr-endpoint-param name="patchRange" .value=${this.patchRange}>
+ </gr-endpoint-param>
+ <gr-endpoint-param name="files" .value=${this.files}>
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ `
+ );
+ }
+
+ private renderDynamicHeaderEndpoints() {
+ return this.dynamicHeaderEndpoints?.map(
+ headerEndpoint => html`
+ <gr-endpoint-decorator
+ class="extra-col"
+ .name=${headerEndpoint}
+ role="columnheader"
+ ></gr-endpoint-decorator>
+ `
+ );
+ }
+
+ private renderShownFiles() {
+ const showDynamicColumns = this.computeShowDynamicColumns();
+ const showPrependedDynamicColumns =
+ this.computeShowPrependedDynamicColumns();
+ const sizeBarLayout = this.computeSizeBarLayout();
+
+ return incrementalRepeat({
+ values: this.shownFiles,
+ mapFn: (f, i) =>
+ this.renderFileRow(
+ f as NormalizedFileInfo,
+ i,
+ sizeBarLayout,
+ showDynamicColumns,
+ showPrependedDynamicColumns
+ ),
+ initialCount: this.fileListIncrement,
+ targetFrameRate: 30,
+ });
+ }
+
+ private renderFileRow(
+ file: NormalizedFileInfo,
+ index: number,
+ sizeBarLayout: SizeBarLayout,
+ showDynamicColumns: boolean,
+ showPrependedDynamicColumns: boolean
+ ) {
+ this.reportRenderedRow(index);
+ const patchSetFile = this.computePatchSetFile(file);
+ return html` <div class="stickyArea">
+ <div
+ class=${`file-row row ${this.computePathClass(file.__path)}`}
+ data-file=${JSON.stringify(patchSetFile)}
+ tabindex="-1"
+ role="row"
+ >
+ <!-- endpoint: change-view-file-list-content-prepend -->
+ ${when(showPrependedDynamicColumns, () =>
+ this.renderPrependedContentEndpointsForFile(file)
+ )}
+ ${this.renderFilePath(file)} ${this.renderFileComments(file)}
+ ${this.renderSizeBar(file, sizeBarLayout)} ${this.renderFileStats(file)}
+ ${when(showDynamicColumns, () =>
+ this.renderDynamicContentEndpointsForFile(file)
+ )}
+ <!-- endpoint: change-view-file-list-content -->
+ ${this.renderReviewed(file)} ${this.renderFileControls(file)}
+ ${this.renderShowHide(file)}
+ </div>
+ ${when(
+ this.isFileExpanded(file.__path),
+ () => html`
+ <gr-diff-host
+ ?noAutoRender=${true}
+ ?showLoadFailure=${true}
+ .displayLine=${this.displayLine}
+ .changeNum=${this.changeNum}
+ .change=${this.change}
+ .patchRange=${this.patchRange}
+ .file=${patchSetFile}
+ .path=${file.__path}
+ .prefs=${this.diffPrefs}
+ .projectName=${this.change?.project}
+ ?noRenderOnPrefsChange=${true}
+ ></gr-diff-host>
+ `
+ )}
+ </div>`;
+ }
+
+ private renderPrependedContentEndpointsForFile(file: NormalizedFileInfo) {
+ return this.dynamicPrependedContentEndpoints?.map(
+ contentEndpoint => html`
+ <gr-endpoint-decorator
+ class="prepended-col"
+ .name=${contentEndpoint}
+ role="gridcell"
+ >
+ <gr-endpoint-param name="change" .value=${this.change}>
+ </gr-endpoint-param>
+ <gr-endpoint-param name="changeNum" .value=${this.changeNum}>
+ </gr-endpoint-param>
+ <gr-endpoint-param name="patchRange" .value=${this.patchRange}>
+ </gr-endpoint-param>
+ <gr-endpoint-param name="path" .value=${file.__path}>
+ </gr-endpoint-param>
+ <gr-endpoint-param name="oldPath" .value=${this.getOldPath(file)}>
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ `
+ );
+ }
+
+ private renderFilePath(file: NormalizedFileInfo) {
+ return html` <span class="path" role="gridcell">
+ <a class="pathLink" href=${ifDefined(this.computeDiffURL(file.__path))}>
+ <span title=${computeDisplayPath(file.__path)} class="fullFileName">
+ ${computeDisplayPath(file.__path)}
+ </span>
+ <span
+ title=${computeDisplayPath(file.__path)}
+ class="truncatedFileName"
+ >
+ ${computeTruncatedPath(file.__path)}
+ </span>
+ <gr-file-status-chip .file=${file}></gr-file-status-chip>
+ <gr-copy-clipboard
+ ?hideInput=${true}
+ .text=${file.__path}
+ ></gr-copy-clipboard>
+ </a>
+ ${when(
+ file.old_path,
+ () => html`
+ <div class="oldPath" title=${ifDefined(file.old_path)}>
+ [[file.old_path]]
+ <gr-copy-clipboard
+ ?hideInput=${true}
+ .text=${file.old_path}
+ ></gr-copy-clipboard>
+ </div>
+ `
+ )}
+ </span>`;
+ }
+
+ private renderFileComments(file: NormalizedFileInfo) {
+ return html` <div role="gridcell">
+ <div class="comments desktop">
+ <span class="drafts">${this.computeDraftsString(file)}</span>
+ <span>${this.computeCommentsString(file)}</span>
+ <span class="noCommentsScreenReaderText">
+ <!-- Screen readers read the following content only if 2 other
+ spans in the parent div is empty. The content is not visible on
+ the page.
+ Without this span, screen readers don't navigate correctly inside
+ table, because empty div doesn't rendered. For example, VoiceOver
+ jumps back to the whole table.
+ We can use   instead, but it sounds worse.
+ -->
+ No comments
+ </span>
+ </div>
+ <div class="comments mobile">
+ <span class="drafts">${this.computeDraftsStringMobile(file)}</span>
+ <span>${this.computeCommentsStringMobile(file)}</span>
+ <span class="noCommentsScreenReaderText">
+ <!-- The same as for desktop comments -->
+ No comments
+ </span>
+ </div>
+ </div>`;
+ }
+
+ private renderSizeBar(
+ file: NormalizedFileInfo,
+ sizeBarLayout: SizeBarLayout
+ ) {
+ return html` <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
+ "Commit message" row content with incorrect column headers.
+ -->
+ <div
+ class=${this.computeSizeBarsClass(file.__path)}
+ aria-label="A bar that represents the addition and deletion ratio for the current file"
+ >
+ <svg width="61" height="8">
+ <rect
+ x=${this.computeBarAdditionX(file, sizeBarLayout)}
+ y="0"
+ height="8"
+ fill="var(--positive-green-text-color)"
+ width=${this.computeBarAdditionWidth(file, sizeBarLayout)}
+ ></rect>
+ <rect
+ x=${this.computeBarDeletionX(sizeBarLayout)}
+ y="0"
+ height="8"
+ fill="var(--negative-red-text-color)"
+ width=${this.computeBarDeletionWidth(file, sizeBarLayout)}
+ ></rect>
+ </svg>
+ </div>
+ </div>`;
+ }
+
+ private renderFileStats(file: NormalizedFileInfo) {
+ return html` <div class="stats" 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
+ "Commit message" row content with incorrect column headers.
+ -->
+ <div class=${this.computeClass('', file.__path)}>
+ <span
+ class="added"
+ tabindex="0"
+ aria-label=${`${file.lines_inserted} lines added`}
+ ?hidden=${file.binary}
+ >
+ +${file.lines_inserted}
+ </span>
+ <span
+ class="removed"
+ tabindex="0"
+ aria-label=${`${file.lines_deleted} lines removed`}
+ ?hidden=${file.binary}
+ >
+ -${file.lines_deleted}
+ </span>
+ <span
+ class=${ifDefined(this.computeBinaryClass(file.size_delta))}
+ ?hidden=${!file.binary}
+ >
+ ${this.formatBytes(file.size_delta)}
+ ${this.formatPercentage(file.size, file.size_delta)}
+ </span>
+ </div>
+ </div>`;
+ }
+
+ private renderDynamicContentEndpointsForFile(file: NormalizedFileInfo) {
+ this.dynamicContentEndpoints?.map(
+ contentEndpoint => html` <div
+ class=${this.computeClass('', file.__path)}
+ role="gridcell"
+ >
+ <gr-endpoint-decorator class="extra-col" .name=${contentEndpoint}>
+ <gr-endpoint-param name="change" .value=${this.change}>
+ </gr-endpoint-param>
+ <gr-endpoint-param name="changeNum" .value=${this.changeNum}>
+ </gr-endpoint-param>
+ <gr-endpoint-param name="patchRange" .value=${this.patchRange}>
+ </gr-endpoint-param>
+ <gr-endpoint-param name="path" .value=${file.__path}>
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </div>`
+ );
+ }
+
+ private renderReviewed(file: NormalizedFileInfo) {
+ if (!this.loggedIn) return nothing;
+ return html` <div class="reviewed hideOnEdit" role="gridcell">
+ <span
+ class=${`reviewedLabel ${this.computeReviewedClass(file.isReviewed)}`}
+ aria-hidden=${this.booleanToString(!file.isReviewed)}
+ >Reviewed</span
+ >
+ <!-- Do not use input type="checkbox" with hidden input and
+ visible label here. Screen readers don't read/interract
+ correctly with such input.
+ -->
+ <span
+ class="reviewedSwitch"
+ role="switch"
+ tabindex="0"
+ @click=${(e: MouseEvent) => this.reviewedClick(e)}
+ @keydown=${(e: KeyboardEvent) => this.reviewedClick(e)}
+ aria-label="Reviewed"
+ aria-checked=${this.booleanToString(file.isReviewed)}
+ >
+ <!-- Trick with tabindex to avoid outline on mouse focus, but
+ preserve focus outline for keyboard navigation -->
+ <span
+ tabindex="-1"
+ class="markReviewed"
+ title=${this.reviewedTitle(file.isReviewed)}
+ >${this.computeReviewedText(file.isReviewed)}</span
+ >
+ </span>
+ </div>`;
+ }
+
+ private renderFileControls(file: NormalizedFileInfo) {
+ return html` <div
+ class="editFileControls showOnEdit"
+ role="gridcell"
+ aria-hidden=${this.booleanToString(!this.editMode)}
+ >
+ ${when(
+ this.editMode,
+ () => html`
+ <gr-edit-file-controls
+ class=${this.computeClass('', file.__path)}
+ .filePath=${file.__path}
+ ></gr-edit-file-controls>
+ `
+ )}
+ </div>`;
+ }
+
+ private renderShowHide(file: NormalizedFileInfo) {
+ return html` <div class="show-hide" role="gridcell">
+ <!-- Do not use input type="checkbox" with hidden input and
+ visible label here. Screen readers don't read/interract
+ correctly with such input.
+ -->
+ <span
+ class="show-hide"
+ data-path=${file.__path}
+ data-expand="true"
+ role="switch"
+ tabindex="0"
+ aria-checked=${this.isFileExpandedStr(file.__path)}
+ aria-label="Expand file"
+ @click=${this.expandedClick}
+ @keydown=${this.expandedClick}
+ >
+ <!-- Trick with tabindex to avoid outline on mouse focus, but
+ preserve focus outline for keyboard navigation -->
+ <iron-icon
+ class="show-hide-icon"
+ tabindex="-1"
+ id="icon"
+ icon=${this.computeShowHideIcon(file.__path)}
+ >
+ </iron-icon>
+ </span>
+ </div>`;
+ }
+
+ private renderCleanlyMerged() {
+ const showPrependedDynamicColumns =
+ this.computeShowPrependedDynamicColumns();
+ return html` <div class="row">
+ <!-- endpoint: change-view-file-list-content-prepend -->
+ ${when(showPrependedDynamicColumns, () =>
+ this.renderPrependedContentEndpoints()
+ )}
+ <div role="gridcell">
+ <div>
+ <span class="cleanlyMergedText">
+ ${this.computeCleanlyMergedText()}
+ </span>
+ <gr-button
+ link
+ class="showParentButton"
+ @click=${this.handleShowParent1}
+ >
+ Show Parent 1
+ </gr-button>
+ </div>
+ </div>
+ </div>`;
+ }
+
+ private renderPrependedContentEndpoints() {
+ return this.dynamicPrependedContentEndpoints?.map(
+ contentEndpoint => html`
+ <gr-endpoint-decorator
+ class="prepended-col"
+ .name=${contentEndpoint}
+ role="gridcell"
+ >
+ <gr-endpoint-param name="change" .value=${this.change}>
+ </gr-endpoint-param>
+ <gr-endpoint-param name="changeNum" .value=${this.changeNum}>
+ </gr-endpoint-param>
+ <gr-endpoint-param name="patchRange" .value=${this.patchRange}>
+ </gr-endpoint-param>
+ <gr-endpoint-param
+ name="cleanlyMergedPaths"
+ .value=${this.cleanlyMergedPaths}
+ >
+ </gr-endpoint-param>
+ <gr-endpoint-param
+ name="cleanlyMergedOldPaths"
+ .value=${this.cleanlyMergedOldPaths}
+ >
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ `
+ );
+ }
+
+ private renderChangeTotals(patchChange: PatchChange) {
+ const showDynamicColumns = this.computeShowDynamicColumns();
+ if (this.shouldHideChangeTotals(patchChange)) return nothing;
+ return html`
+ <div class="row totalChanges">
+ <div class="total-stats">
+ <div>
+ <span
+ class="added"
+ tabindex="0"
+ aria-label="Total ${patchChange.inserted} lines added"
+ >
+ +${patchChange.inserted}
+ </span>
+ <span
+ class="removed"
+ tabindex="0"
+ aria-label="Total ${patchChange.deleted} lines removed"
+ >
+ -${patchChange.deleted}
+ </span>
+ </div>
+ </div>
+ ${when(showDynamicColumns, () =>
+ this.dynamicSummaryEndpoints?.map(
+ summaryEndpoint => html`
+ <gr-endpoint-decorator class="extra-col" name=${summaryEndpoint}>
+ <gr-endpoint-param name="change" .value=${this.change}>
+ </gr-endpoint-param>
+ <gr-endpoint-param name="patchRange" .value=${this.patchRange}>
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ `
+ )
+ )}
+
+ <!-- Empty div here exists to keep spacing in sync with file rows. -->
+ <div class="reviewed hideOnEdit" ?hidden=${!this.loggedIn}></div>
+ <div class="editFileControls showOnEdit"></div>
+ <div class="show-hide"></div>
+ </div>
+ `;
+ }
+
+ private renderBinaryTotals(patchChange: PatchChange) {
+ if (this.shouldHideBinaryChangeTotals(patchChange)) return nothing;
+ const deltaInserted = this.formatBytes(patchChange.size_delta_inserted);
+ const deltaDeleted = this.formatBytes(patchChange.size_delta_deleted);
+ return html`
+ <div class="row totalChanges">
+ <div class="total-stats">
+ <span
+ class="added"
+ aria-label="Total bytes inserted: ${deltaInserted}"
+ >
+ ${deltaInserted}
+ ${this.formatPercentage(
+ patchChange.total_size,
+ patchChange.size_delta_inserted
+ )}
+ </span>
+ <span
+ class="removed"
+ aria-label="Total bytes removed: ${deltaDeleted}"
+ >
+ ${deltaDeleted}
+ ${this.formatPercentage(
+ patchChange.total_size,
+ patchChange.size_delta_deleted
+ )}
+ </span>
+ </div>
+ </div>
+ `;
+ }
+
+ private renderControlRow() {
+ return html`<div
+ class=${`row controlRow ${this.computeFileListControlClass()}`}
+ >
+ <gr-button
+ class="fileListButton"
+ id="incrementButton"
+ link=""
+ @click=${this.incrementNumFilesShown}
+ >
+ ${this.computeIncrementText()}
+ </gr-button>
+ <gr-tooltip-content
+ ?has-tooltip=${this.computeWarnShowAll()}
+ ?show-icon=${this.computeWarnShowAll()}
+ .title=${this.computeShowAllWarning()}
+ >
+ <gr-button
+ class="fileListButton"
+ id="showAllButton"
+ link=""
+ @click=${this.showAllFiles}
+ >
+ ${this.computeShowAllText()}
+ </gr-button>
+ </gr-tooltip-content>
+ </div>`;
+ }
+
reload() {
if (!this.changeNum || !this.patchRange?.patchNum) {
return Promise.resolve();
@@ -476,7 +1412,7 @@
const changeNum = this.changeNum;
const patchRange = this.patchRange;
- this._loading = true;
+ this.loading = true;
this.collapseAllDiffs();
const promises: Promise<boolean | void>[] = [];
@@ -485,23 +1421,22 @@
this.restApiService
.getChangeOrEditFiles(changeNum, patchRange)
.then(filesByPath => {
- this._filesByPath = filesByPath;
+ this.filesByPath = filesByPath;
})
);
promises.push(
- this._getLoggedIn().then(loggedIn => (this._loggedIn = loggedIn))
+ this.getLoggedIn().then(loggedIn => (this.loggedIn = loggedIn))
);
return Promise.all(promises).then(() => {
- this._loading = false;
- this._detectChromiteButler();
+ this.loading = false;
+ this.detectChromiteButler();
this.reporting.fileListDisplayed();
});
}
- @observe('_filesByPath')
- async _updateCleanlyMergedPaths(filesByPath?: FileNameToFileInfoMap) {
+ private async updateCleanlyMergedPaths() {
// When viewing Auto Merge base vs a patchset, add an additional row that
// knows how many files were cleanly merged. This requires an additional RPC
// for the diffs between target parent and the patch set. The cleanly merged
@@ -522,21 +1457,21 @@
patchNum: this.patchRange.patchNum,
}
);
- if (!allFilesByPath || !filesByPath) return;
- const conflictingPaths = Object.keys(filesByPath);
- this._cleanlyMergedPaths = Object.keys(allFilesByPath).filter(
+ if (!allFilesByPath || !this.filesByPath) return;
+ const conflictingPaths = Object.keys(this.filesByPath);
+ this.cleanlyMergedPaths = Object.keys(allFilesByPath).filter(
path => !conflictingPaths.includes(path)
);
- this._cleanlyMergedOldPaths = this._cleanlyMergedPaths
+ this.cleanlyMergedOldPaths = this.cleanlyMergedPaths
.map(path => allFilesByPath[path].old_path)
.filter((oldPath): oldPath is string => !!oldPath);
} else {
- this._cleanlyMergedPaths = [];
- this._cleanlyMergedOldPaths = [];
+ this.cleanlyMergedPaths = [];
+ this.cleanlyMergedOldPaths = [];
}
}
- _detectChromiteButler() {
+ private detectChromiteButler() {
const hasButler = !!document.getElementById('butler-suggested-owners');
if (hasButler) {
this.reporting.reportExtension('butler');
@@ -544,7 +1479,7 @@
}
get diffs(): GrDiffHost[] {
- const diffs = this.root!.querySelectorAll('gr-diff-host');
+ const diffs = this.shadowRoot!.querySelectorAll('gr-diff-host');
// It is possible that a bogus diff element is hanging around invisibly
// from earlier with a different patch set choice and associated with a
// different entry in the files array. So filter on visible items only.
@@ -554,12 +1489,13 @@
}
openDiffPrefs() {
- this.$.diffPreferencesDialog.open();
+ this.diffPreferencesDialog?.open();
}
- _calculatePatchChange(files: NormalizedFileInfo[]): PatchChange {
- const magicFilesExcluded = files.filter(
- files => !isMagicPath(files.__path)
+ // Private but used in tests.
+ calculatePatchChange(): PatchChange {
+ const magicFilesExcluded = this.files.filter(
+ file => !isMagicPath(file.__path)
);
return magicFilesExcluded.reduce((acc, obj) => {
@@ -582,40 +1518,39 @@
}
// private but used in test
- _toggleFileExpanded(file: PatchSetFile) {
+ toggleFileExpanded(file: PatchSetFile) {
// Is the path in the list of expanded diffs? If so, remove it, otherwise
// add it to the list.
- const indexInExpanded = this._expandedFiles.findIndex(
+ const indexInExpanded = this.expandedFiles.findIndex(
f => f.path === file.path
);
if (indexInExpanded === -1) {
- this.push('_expandedFiles', file);
+ this.expandedFiles = this.expandedFiles.concat([file]);
} else {
- this.splice('_expandedFiles', indexInExpanded, 1);
+ this.expandedFiles = this.expandedFiles.filter(
+ (_val, idx) => idx !== indexInExpanded
+ );
}
- const indexInAll = this._files.findIndex(f => f.__path === file.path);
- this.root!.querySelectorAll(`.${FILE_ROW_CLASS}`)[
+ const indexInAll = this.files.findIndex(f => f.__path === file.path);
+ this.shadowRoot!.querySelectorAll(`.${FILE_ROW_CLASS}`)[
indexInAll
].scrollIntoView({block: 'nearest'});
}
- _toggleFileExpandedByIndex(index: number) {
- this._toggleFileExpanded(this._computePatchSetFile(this._files[index]));
+ private toggleFileExpandedByIndex(index: number) {
+ this.toggleFileExpanded(this.computePatchSetFile(this.files[index]));
}
- _updateDiffPreferences() {
+ // Private but used in tests.
+ updateDiffPreferences() {
if (!this.diffs.length) {
return;
}
// Re-render all expanded diffs sequentially.
- this._renderInOrder(
- this._expandedFiles,
- this.diffs,
- this._expandedFiles.length
- );
+ this.renderInOrder(this.expandedFiles, this.diffs);
}
- _forEachDiff(fn: (host: GrDiffHost) => void) {
+ private forEachDiff(fn: (host: GrDiffHost) => void) {
const diffs = this.diffs;
for (let i = 0; i < diffs.length; i++) {
fn(diffs[i]);
@@ -627,54 +1562,45 @@
// expanded list.
const newFiles: PatchSetFile[] = [];
let path: string;
- for (let i = 0; i < this._shownFiles.length; i++) {
- path = this._shownFiles[i].__path;
- if (!this._expandedFiles.some(f => f.path === path)) {
- newFiles.push(this._computePatchSetFile(this._shownFiles[i]));
+ for (let i = 0; i < this.shownFiles.length; i++) {
+ path = this.shownFiles[i].__path;
+ if (!this.expandedFiles.some(f => f.path === path)) {
+ newFiles.push(this.computePatchSetFile(this.shownFiles[i]));
}
}
- this.splice('_expandedFiles', 0, 0, ...newFiles);
+ this.expandedFiles = newFiles.concat(this.expandedFiles);
}
collapseAllDiffs() {
- this._expandedFiles = [];
- this.filesExpanded = this._computeExpandedFiles(
- this._expandedFiles.length,
- this._files.length
- );
- this.diffCursor.handleDiffUpdate();
+ this.expandedFiles = [];
}
/**
* Computes a string with the number of comments and unresolved comments.
*/
- _computeCommentsString(
- changeComments?: ChangeComments,
- patchRange?: PatchRange,
- file?: NormalizedFileInfo
- ) {
+ computeCommentsString(file?: NormalizedFileInfo) {
if (
- changeComments === undefined ||
- patchRange === undefined ||
+ this.changeComments === undefined ||
+ this.patchRange === undefined ||
file?.__path === undefined
) {
return '';
}
- return changeComments.computeCommentsString(patchRange, file.__path, file);
+ return this.changeComments.computeCommentsString(
+ this.patchRange,
+ file.__path,
+ file
+ );
}
/**
* Computes a string with the number of drafts.
*/
- _computeDraftsString(
- changeComments?: ChangeComments,
- patchRange?: PatchRange,
- file?: NormalizedFileInfo
- ) {
- if (changeComments === undefined) return '';
- const draftCount = changeComments.computeDraftCountForFile(
- patchRange,
+ computeDraftsString(file?: NormalizedFileInfo) {
+ if (this.changeComments === undefined) return '';
+ const draftCount = this.changeComments.computeDraftCountForFile(
+ this.patchRange,
file
);
if (draftCount === 0) return '';
@@ -683,15 +1609,12 @@
/**
* Computes a shortened string with the number of drafts.
+ * Private but used in tests.
*/
- _computeDraftsStringMobile(
- changeComments?: ChangeComments,
- patchRange?: PatchRange,
- file?: NormalizedFileInfo
- ) {
- if (changeComments === undefined) return '';
- const draftCount = changeComments.computeDraftCountForFile(
- patchRange,
+ computeDraftsStringMobile(file?: NormalizedFileInfo) {
+ if (this.changeComments === undefined) return '';
+ const draftCount = this.changeComments.computeDraftCountForFile(
+ this.patchRange,
file
);
return draftCount === 0 ? '' : `${draftCount}d`;
@@ -700,43 +1623,38 @@
/**
* Computes a shortened string with the number of comments.
*/
- _computeCommentsStringMobile(
- changeComments?: ChangeComments,
- patchRange?: PatchRange,
- file?: NormalizedFileInfo
- ) {
+ computeCommentsStringMobile(file?: NormalizedFileInfo) {
if (
- changeComments === undefined ||
- patchRange === undefined ||
+ this.changeComments === undefined ||
+ this.patchRange === undefined ||
file === undefined
) {
return '';
}
const commentThreadCount =
- changeComments.computeCommentThreadCount({
- patchNum: patchRange.basePatchNum,
+ this.changeComments.computeCommentThreadCount({
+ patchNum: this.patchRange.basePatchNum,
path: file.__path,
}) +
- changeComments.computeCommentThreadCount({
- patchNum: patchRange.patchNum,
+ this.changeComments.computeCommentThreadCount({
+ patchNum: this.patchRange.patchNum,
path: file.__path,
});
return commentThreadCount === 0 ? '' : `${commentThreadCount}c`;
}
- // private but used in test
- _reviewFile(path: string, reviewed?: boolean) {
+ // Private but used in tests.
+ reviewFile(path: string, reviewed?: boolean) {
if (this.editMode) {
return Promise.resolve();
}
- const index = this._files.findIndex(file => file.__path === path);
- reviewed = reviewed || !this._files[index].isReviewed;
-
- this.set(['_files', index, 'isReviewed'], reviewed);
- if (index < this._shownFiles.length) {
- this.notifyPath(`_shownFiles.${index}.isReviewed`);
+ const index = this.files.findIndex(file => file.__path === path);
+ reviewed = reviewed || !this.files[index].isReviewed;
+ this.files[index].isReviewed = reviewed;
+ if (index < this.shownFiles.length) {
+ this.requestUpdate('shownFiles');
}
-
+ this.requestUpdate('files');
return this._saveReviewedState(path, reviewed);
}
@@ -753,18 +1671,11 @@
);
}
- _getLoggedIn() {
+ private getLoggedIn() {
return this.restApiService.getLoggedIn();
}
- _getReviewedFiles(changeNum: NumericChangeId, patchRange: PatchRange) {
- if (this.editMode) {
- return Promise.resolve([]);
- }
- return this.restApiService.getReviewedFiles(changeNum, patchRange.patchNum);
- }
-
- _normalizeChangeFilesResponse(
+ private normalizeChangeFilesResponse(
response: FileNameToReviewedFileInfoMap
): NormalizedFileInfo[] {
const paths = Object.keys(response).sort(specialFilePathCompare);
@@ -786,7 +1697,7 @@
* The click is: mouse click or pressing Enter or Space key
* P.S> Screen readers sends click event as well
*/
- _isClickEvent(e: MouseEvent | KeyboardEvent) {
+ private isClickEvent(e: MouseEvent | KeyboardEvent) {
if (e.type === 'click') {
return true;
}
@@ -795,41 +1706,43 @@
return ke.type === 'keydown' && isSpaceOrEnter;
}
- _fileActionClick(
+ private fileActionClick(
e: MouseEvent | KeyboardEvent,
fileAction: (file: PatchSetFile) => void
) {
- if (this._isClickEvent(e)) {
- const fileRow = this._getFileRowFromEvent(e);
+ if (this.isClickEvent(e)) {
+ const fileRow = this.getFileRowFromEvent(e);
if (!fileRow) {
return;
}
// Prevent default actions (e.g. scrolling for space key)
e.preventDefault();
- // Prevent _handleFileListClick handler call
+ // Prevent handleFileListClick handler call
e.stopPropagation();
this.fileCursor.setCursor(fileRow.element);
fileAction(fileRow.file);
}
}
- _reviewedClick(e: MouseEvent | KeyboardEvent) {
- this._fileActionClick(e, file => this._reviewFile(file.path));
+ // Private but used in tests.
+ reviewedClick(e: MouseEvent | KeyboardEvent) {
+ this.fileActionClick(e, file => this.reviewFile(file.path));
}
- _expandedClick(e: MouseEvent | KeyboardEvent) {
- this._fileActionClick(e, file => this._toggleFileExpanded(file));
+ private expandedClick(e: MouseEvent | KeyboardEvent) {
+ this.fileActionClick(e, file => this.toggleFileExpanded(file));
}
/**
* Handle all events from the file list dom-repeat so event handlers don't
* have to get registered for potentially very long lists.
+ * Private but used in tests.
*/
- _handleFileListClick(e: MouseEvent) {
+ handleFileListClick(e: MouseEvent) {
if (!e.target) {
return;
}
- const fileRow = this._getFileRowFromEvent(e);
+ const fileRow = this.getFileRowFromEvent(e);
if (!fileRow) {
return;
}
@@ -849,10 +1762,10 @@
e.preventDefault();
this.fileCursor.setCursor(fileRow.element);
- this._toggleFileExpanded(file);
+ this.toggleFileExpanded(file);
}
- _getFileRowFromEvent(e: Event): FileRow | null {
+ private getFileRowFromEvent(e: Event): FileRow | null {
// Traverse upwards to find the row element if the target is not the row.
let row = e.target as HTMLElement;
while (!row.classList.contains(FILE_ROW_CLASS) && row.parentElement) {
@@ -873,7 +1786,7 @@
/**
* Generates file range from file info object.
*/
- _computePatchSetFile(file: NormalizedFileInfo): PatchSetFile {
+ private computePatchSetFile(file: NormalizedFileInfo): PatchSetFile {
const fileData: PatchSetFile = {
path: file.__path,
};
@@ -883,90 +1796,95 @@
return fileData;
}
- _handleLeftPane() {
- if (this._noDiffsExpanded()) return;
+ private handleLeftPane() {
+ if (this.noDiffsExpanded()) return;
this.diffCursor.moveLeft();
}
- _handleRightPane() {
- if (this._noDiffsExpanded()) return;
+ private handleRightPane() {
+ if (this.noDiffsExpanded()) return;
this.diffCursor.moveRight();
}
- _handleToggleInlineDiff() {
+ private handleToggleInlineDiff() {
if (this.fileCursor.index === -1) return;
- this._toggleFileExpandedByIndex(this.fileCursor.index);
+ this.toggleFileExpandedByIndex(this.fileCursor.index);
}
- _handleCursorNext(e: KeyboardEvent) {
+ // Private but used in tests.
+ handleCursorNext(e: KeyboardEvent) {
if (this.filesExpanded === FilesExpandedState.ALL) {
this.diffCursor.moveDown();
- this._displayLine = true;
+ this.displayLine = true;
} else {
if (e.key === Key.DOWN) return;
this.fileCursor.next({circular: true});
this.selectedIndex = this.fileCursor.index;
+ fire(this, 'selected-index-changed', {value: this.fileCursor.index});
}
}
- _handleCursorPrev(e: KeyboardEvent) {
+ // Private but used in tests.
+ handleCursorPrev(e: KeyboardEvent) {
if (this.filesExpanded === FilesExpandedState.ALL) {
this.diffCursor.moveUp();
- this._displayLine = true;
+ this.displayLine = true;
} else {
if (e.key === Key.UP) return;
this.fileCursor.previous({circular: true});
this.selectedIndex = this.fileCursor.index;
+ fire(this, 'selected-index-changed', {value: this.fileCursor.index});
}
}
- _handleNewComment() {
+ private handleNewComment() {
this.classList.remove('hideComments');
this.diffCursor.createCommentInPlace();
}
+ // Private but used in tests.
handleOpenFile() {
if (this.filesExpanded === FilesExpandedState.ALL) {
- this._openCursorFile();
+ this.openCursorFile();
return;
}
- this._openSelectedFile();
+ this.openSelectedFile();
}
- _handleNextChunk() {
- if (this._noDiffsExpanded()) return;
+ private handleNextChunk() {
+ if (this.noDiffsExpanded()) return;
this.diffCursor.moveToNextChunk();
}
- _handleNextComment() {
- if (this._noDiffsExpanded()) return;
+ private handleNextComment() {
+ if (this.noDiffsExpanded()) return;
this.diffCursor.moveToNextCommentThread();
}
- _handlePrevChunk() {
- if (this._noDiffsExpanded()) return;
+ private handlePrevChunk() {
+ if (this.noDiffsExpanded()) return;
this.diffCursor.moveToPreviousChunk();
}
- _handlePrevComment() {
- if (this._noDiffsExpanded()) return;
+ private handlePrevComment() {
+ if (this.noDiffsExpanded()) return;
this.diffCursor.moveToPreviousCommentThread();
}
- _handleToggleFileReviewed() {
- if (!this._files[this.fileCursor.index]) {
+ private handleToggleFileReviewed() {
+ if (!this.files[this.fileCursor.index]) {
return;
}
- this._reviewFile(this._files[this.fileCursor.index].__path);
+ this.reviewFile(this.files[this.fileCursor.index].__path);
}
- _handleToggleLeftPane() {
- this._forEachDiff(diff => {
+ private handleToggleLeftPane() {
+ this.forEachDiff(diff => {
diff.toggleLeftDiff();
});
}
- _toggleInlineDiffs() {
+ private toggleInlineDiffs() {
if (this.filesExpanded === FilesExpandedState.ALL) {
this.collapseAllDiffs();
} else {
@@ -974,7 +1892,8 @@
}
}
- _openCursorFile() {
+ // Private but used in tests.
+ openCursorFile() {
const diff = this.diffCursor.getTargetDiffElement();
if (!this.change || !diff || !this.patchRange || !diff.path) {
throw new Error('change, diff and patchRange must be all set and valid');
@@ -987,11 +1906,12 @@
);
}
- _openSelectedFile(index?: number) {
+ // Private but used in tests.
+ openSelectedFile(index?: number) {
if (index !== undefined) {
this.fileCursor.setCursorAtIndex(index);
}
- if (!this._files[this.fileCursor.index]) {
+ if (!this.files[this.fileCursor.index]) {
return;
}
if (!this.change || !this.patchRange) {
@@ -999,45 +1919,52 @@
}
GerritNav.navigateToDiff(
this.change,
- this._files[this.fileCursor.index].__path,
+ this.files[this.fileCursor.index].__path,
this.patchRange.patchNum,
this.patchRange.basePatchNum
);
}
- _shouldHideChangeTotals(_patchChange: PatchChange): boolean {
- return _patchChange.inserted === 0 && _patchChange.deleted === 0;
+ // Private but used in tests.
+ shouldHideChangeTotals(patchChange: PatchChange): boolean {
+ return patchChange.inserted === 0 && patchChange.deleted === 0;
}
- _shouldHideBinaryChangeTotals(_patchChange: PatchChange) {
+ // Private but used in tests.
+ shouldHideBinaryChangeTotals(patchChange: PatchChange) {
return (
- _patchChange.size_delta_inserted === 0 &&
- _patchChange.size_delta_deleted === 0
+ patchChange.size_delta_inserted === 0 &&
+ patchChange.size_delta_deleted === 0
);
}
- _computeDiffURL(
- change?: ParsedChangeInfo,
- basePatchNum?: BasePatchSetNum,
- patchNum?: RevisionPatchSetNum,
- path?: string,
- editMode?: boolean
- ) {
+ // Private but used in tests
+ computeDiffURL(path?: string) {
if (
- change === undefined ||
- patchNum === undefined ||
+ this.change === undefined ||
+ this.patchRange?.patchNum === undefined ||
path === undefined ||
- editMode === undefined
+ this.editMode === undefined
) {
return;
}
- if (editMode && path !== SpecialFilePath.MERGE_LIST) {
- return GerritNav.getEditUrlForDiff(change, path, patchNum);
+ if (this.editMode && path !== SpecialFilePath.MERGE_LIST) {
+ return GerritNav.getEditUrlForDiff(
+ this.change,
+ path,
+ this.patchRange.patchNum
+ );
}
- return GerritNav.getUrlForDiff(change, path, patchNum, basePatchNum);
+ return GerritNav.getUrlForDiff(
+ this.change,
+ path,
+ this.patchRange.patchNum,
+ this.patchRange.basePatchNum
+ );
}
- _formatBytes(bytes?: number) {
+ // Private but used in tests.
+ formatBytes(bytes?: number) {
if (!bytes) return '+/-0 B';
const bits = 1024;
const decimals = 1;
@@ -1050,7 +1977,8 @@
return `${prepend}${value} ${sizes[exponent]}`;
}
- _formatPercentage(size?: number, delta?: number) {
+ // Private but used in tests.
+ formatPercentage(size?: number, delta?: number) {
if (size === undefined || delta === undefined) {
return '';
}
@@ -1064,14 +1992,14 @@
return `(${delta > 0 ? '+' : '-'}${percentage}%)`;
}
- _computeBinaryClass(delta?: number) {
+ private computeBinaryClass(delta?: number) {
if (!delta) {
return;
}
return delta > 0 ? 'added' : 'removed';
}
- _computeClass(baseClass?: string, path?: string) {
+ private computeClass(baseClass?: string, path?: string) {
const classes = [];
if (baseClass) {
classes.push(baseClass);
@@ -1085,32 +2013,26 @@
return classes.join(' ');
}
- _computePathClass(
- path: string | undefined,
- expandedFilesRecord: ElementPropertyDeepChange<GrFileList, '_expandedFiles'>
- ) {
- return this._isFileExpanded(path, expandedFilesRecord) ? 'expanded' : '';
+ private computePathClass(path: string | undefined) {
+ return this.isFileExpanded(path) ? 'expanded' : '';
}
- _computeShowHideIcon(
- path: string | undefined,
- expandedFilesRecord: ElementPropertyDeepChange<GrFileList, '_expandedFiles'>
- ) {
- return this._isFileExpanded(path, expandedFilesRecord)
+ private computeShowHideIcon(path: string | undefined) {
+ return this.isFileExpanded(path)
? 'gr-icons:expand-less'
: 'gr-icons:expand-more';
}
- _computeShowNumCleanlyMerged(cleanlyMergedPaths: string[]): boolean {
- return cleanlyMergedPaths.length > 0;
+ private computeShowNumCleanlyMerged(): boolean {
+ return this.cleanlyMergedPaths.length > 0;
}
- _computeCleanlyMergedText(cleanlyMergedPaths: string[]): string {
- const fileCount = pluralize(cleanlyMergedPaths.length, 'file');
+ private computeCleanlyMergedText(): string {
+ const fileCount = pluralize(this.cleanlyMergedPaths.length, 'file');
return `${fileCount} merged cleanly in Parent 1`;
}
- _handleShowParent1(): void {
+ private handleShowParent1(): void {
if (!this.change || !this.patchRange) return;
GerritNav.navigateToChange(this.change, {
patchNum: this.patchRange.patchNum,
@@ -1118,56 +2040,34 @@
});
}
- @observe(
- '_filesByPath',
- 'changeComments',
- 'patchRange',
- 'reviewed',
- '_loading'
- )
- _computeFiles(
- filesByPath?: FileNameToFileInfoMap,
- changeComments?: ChangeComments,
- patchRange?: PatchRange,
- reviewed?: string[],
- loading?: boolean
- ) {
- // Polymer 2: check for undefined
+ private computeFiles() {
if (
- filesByPath === undefined ||
- changeComments === undefined ||
- patchRange === undefined ||
- reviewed === undefined ||
- loading === undefined
+ this.filesByPath === undefined ||
+ this.changeComments === undefined ||
+ this.patchRange === undefined ||
+ this.reviewed === undefined ||
+ this.loading === undefined
) {
return;
}
// Await all promises resolving from reload. @See Issue 9057
- if (loading || !changeComments) {
+ if (this.loading || !this.changeComments) {
return;
}
- const commentedPaths = changeComments.getPaths(patchRange);
- const files: FileNameToReviewedFileInfoMap = {...filesByPath};
+ const commentedPaths = this.changeComments.getPaths(this.patchRange);
+ const files: FileNameToReviewedFileInfoMap = {...this.filesByPath};
addUnmodifiedFiles(files, commentedPaths);
- const reviewedSet = new Set(reviewed || []);
+ const reviewedSet = new Set(this.reviewed || []);
for (const [filePath, reviewedFileInfo] of Object.entries(files)) {
reviewedFileInfo.isReviewed = reviewedSet.has(filePath);
}
- this._files = this._normalizeChangeFilesResponse(files);
+ this.files = this.normalizeChangeFilesResponse(files);
}
- _computeFilesShown(
- numFilesShown: number,
- files: NormalizedFileInfo[]
- ): NormalizedFileInfo[] | undefined {
- // Polymer 2: check for undefined
- if (numFilesShown === undefined || files === undefined) return undefined;
+ private computeFilesShown(): NormalizedFileInfo[] {
+ const previousNumFilesShown = this.shownFiles ? this.shownFiles.length : 0;
- const previousNumFilesShown = this._shownFiles
- ? this._shownFiles.length
- : 0;
-
- const filesShown = files.slice(0, numFilesShown);
+ const filesShown = this.files.slice(0, this.numFilesShown);
this.dispatchEvent(
new CustomEvent('files-shown-changed', {
detail: {length: filesShown.length},
@@ -1177,12 +2077,12 @@
);
// Start the timer for the rendering work hwere because this is where the
- // _shownFiles property is being set, and _shownFiles is used in the
+ // shownFiles property is being set, and shownFiles is used in the
// dom-repeat binding.
this.reporting.time(Timing.FILE_RENDER);
// How many more files are being shown (if it's an increase).
- this._reportinShownFilesIncrement = Math.max(
+ this.reportinShownFilesIncrement = Math.max(
0,
filesShown.length - previousNumFilesShown
);
@@ -1190,59 +2090,56 @@
return filesShown;
}
- _updateDiffCursor() {
+ // Private but used in tests.
+ updateDiffCursor() {
// Overwrite the cursor's list of diffs:
this.diffCursor.replaceDiffs(this.diffs);
}
- _filesChanged() {
- if (this._files && this._files.length > 0) {
- flush();
- this.fileCursor.stops = Array.from(
- this.root!.querySelectorAll(`.${FILE_ROW_CLASS}`)
- );
- this.fileCursor.setCursorAtIndex(this.selectedIndex, true);
- }
+ async filesChanged() {
+ if (!this.files || this.files.length === 0) return;
+ await this.updateComplete;
+ this.fileCursor.stops = Array.from(
+ this.shadowRoot?.querySelectorAll(`.${FILE_ROW_CLASS}`) ?? []
+ );
+ this.fileCursor.setCursorAtIndex(this.selectedIndex, true);
}
- _incrementNumFilesShown() {
+ private incrementNumFilesShown() {
this.numFilesShown += this.fileListIncrement;
+ fire(this, 'num-files-shown-changed', {value: this.numFilesShown});
}
- _computeFileListControlClass(
- numFilesShown?: number,
- files?: NormalizedFileInfo[]
- ) {
- if (numFilesShown === undefined || files === undefined) return 'invisible';
- return numFilesShown >= files.length ? 'invisible' : '';
+ private computeFileListControlClass() {
+ return this.numFilesShown >= this.files.length ? 'invisible' : '';
}
- _computeIncrementText(numFilesShown?: number, files?: NormalizedFileInfo[]) {
- if (numFilesShown === undefined || files === undefined) return '';
- const text = Math.min(this.fileListIncrement, files.length - numFilesShown);
+ private computeIncrementText() {
+ const text = Math.min(
+ this.fileListIncrement,
+ this.files.length - this.numFilesShown
+ );
return `Show ${text} more`;
}
- _computeShowAllText(files: NormalizedFileInfo[]) {
- if (!files) {
+ private computeShowAllText() {
+ return `Show all ${this.files.length} files`;
+ }
+
+ private computeWarnShowAll() {
+ return this.files.length > WARN_SHOW_ALL_THRESHOLD;
+ }
+
+ private computeShowAllWarning() {
+ if (!this.computeWarnShowAll()) {
return '';
}
- return `Show all ${files.length} files`;
+ return `Warning: showing all ${this.files.length} files may take several seconds.`;
}
- _computeWarnShowAll(files: NormalizedFileInfo[]) {
- return files.length > WARN_SHOW_ALL_THRESHOLD;
- }
-
- _computeShowAllWarning(files: NormalizedFileInfo[]) {
- if (!this._computeWarnShowAll(files)) {
- return '';
- }
- return `Warning: showing all ${files.length} files may take several seconds.`;
- }
-
- _showAllFiles() {
- this.numFilesShown = this._files.length;
+ private showAllFiles() {
+ this.numFilesShown = this.files.length;
+ fire(this, 'num-files-shown-changed', {value: this.numFilesShown});
}
/**
@@ -1254,33 +2151,22 @@
*
* @return 'true' if val is true-like, otherwise false
*/
- _booleanToString(val?: unknown) {
+ private booleanToString(val?: unknown) {
return val ? 'true' : 'false';
}
- _isFileExpanded(
- path: string | undefined,
- expandedFilesRecord: ElementPropertyDeepChange<GrFileList, '_expandedFiles'>
- ) {
- return expandedFilesRecord.base.some(f => f.path === path);
+ private isFileExpanded(path: string | undefined) {
+ return this.expandedFiles.some(f => f.path === path);
}
- _isFileExpandedStr(
- path: string | undefined,
- expandedFilesRecord: ElementPropertyDeepChange<GrFileList, '_expandedFiles'>
- ) {
- return this._booleanToString(
- this._isFileExpanded(path, expandedFilesRecord)
- );
+ private isFileExpandedStr(path: string | undefined) {
+ return this.booleanToString(this.isFileExpanded(path));
}
- private _computeExpandedFiles(
- expandedCount: number,
- totalCount: number
- ): FilesExpandedState {
- if (expandedCount === 0) {
+ private computeExpandedFiles(): FilesExpandedState {
+ if (this.expandedFiles.length === 0) {
return FilesExpandedState.NONE;
- } else if (expandedCount === totalCount) {
+ } else if (this.expandedFiles.length === this.files.length) {
return FilesExpandedState.ALL;
}
return FilesExpandedState.SOME;
@@ -1292,44 +2178,35 @@
* order by waiting for the previous diff to finish before starting the next
* one.
*
- * @param record The splice record in the expanded paths list.
+ * @param newFiles The new files that have been added.
+ * Private but used in tests.
*/
- @observe('_expandedFiles.splices')
- _expandedFilesChanged(record?: PolymerSpliceChange<PatchSetFile[]>) {
+ async expandedFilesChanged(oldFiles: Array<PatchSetFile>) {
// Clear content for any diffs that are not open so if they get re-opened
// the stale content does not flash before it is cleared and reloaded.
const collapsedDiffs = this.diffs.filter(
- diff => this._expandedFiles.findIndex(f => f.path === diff.path) === -1
+ diff => this.expandedFiles.findIndex(f => f.path === diff.path) === -1
);
- this._clearCollapsedDiffs(collapsedDiffs);
+ this.clearCollapsedDiffs(collapsedDiffs);
- if (!record) {
- return;
- } // Happens after "Collapse all" clicked.
+ this.filesExpanded = this.computeExpandedFiles();
- this.filesExpanded = this._computeExpandedFiles(
- this._expandedFiles.length,
- this._files.length
- );
-
- // Find the paths introduced by the new index splices:
- const newFiles = record.indexSplices.flatMap(splice =>
- splice.object.slice(splice.index, splice.index + splice.addedCount)
+ const newFiles = this.expandedFiles.filter(
+ file => (oldFiles ?? []).findIndex(f => f.path === file.path) === -1
);
// Required so that the newly created diff view is included in this.diffs.
- flush();
+ await this.updateComplete;
if (newFiles.length) {
- this._renderInOrder(newFiles, this.diffs, newFiles.length);
+ await this.renderInOrder(newFiles, this.diffs);
}
-
- this._updateDiffCursor();
+ this.updateDiffCursor();
this.diffCursor.reInitAndUpdateStops();
}
// private but used in test
- _clearCollapsedDiffs(collapsedDiffs: GrDiffHost[]) {
+ clearCollapsedDiffs(collapsedDiffs: GrDiffHost[]) {
for (const diff of collapsedDiffs) {
diff.cancel();
diff.clearDiffContent();
@@ -1345,31 +2222,31 @@
*
* @param initialCount The total number of paths in the pass.
*/
- async _renderInOrder(
- files: PatchSetFile[],
- diffElements: GrDiffHost[],
- initialCount: number
- ) {
+ async renderInOrder(files: PatchSetFile[], diffElements: GrDiffHost[]) {
this.reporting.time(Timing.FILE_EXPAND_ALL);
for (const file of files) {
const path = file.path;
- const diffElem = this._findDiffByPath(path, diffElements);
- if (diffElem) {
- diffElem.prefetchDiff();
- }
- }
-
- await asyncForeach(files, (file, cancel) => {
- const path = file.path;
- this._cancelForEachDiff = cancel;
-
- const diffElem = this._findDiffByPath(path, diffElements);
+ const diffElem = this.findDiffByPath(path, diffElements);
if (!diffElem) {
this.reporting.error(
new Error(`Did not find <gr-diff-host> element for ${path}`)
);
- return Promise.resolve();
+ return;
+ }
+ diffElem.prefetchDiff();
+ }
+
+ await asyncForeach(files, async (file, cancel) => {
+ const path = file.path;
+ this.cancelForEachDiff = cancel;
+
+ const diffElem = this.findDiffByPath(path, diffElements);
+ if (!diffElem) {
+ this.reporting.error(
+ new Error(`Did not find <gr-diff-host> element for ${path}`)
+ );
+ return;
}
if (!this.diffPrefs) {
throw new Error('diffPrefs must be set');
@@ -1381,18 +2258,18 @@
// control over which diffs were actually seen. And for lots of diffs
// that would even be a problem for write QPS quota.
if (
- this._loggedIn &&
+ this.loggedIn &&
!this.diffPrefs.manual_review &&
- initialCount === 1
+ files.length === 1
) {
- this._reviewFile(path, true);
+ this.reviewFile(path, true);
}
- return diffElem.reload();
+ await diffElem.reload();
});
- this._cancelForEachDiff = undefined;
+ this.cancelForEachDiff = undefined;
this.reporting.timeEnd(Timing.FILE_EXPAND_ALL, {
- count: initialCount,
+ count: files.length,
height: this.clientHeight,
});
/* Block diff cursor from auto scrolling after files are done rendering.
@@ -1411,17 +2288,17 @@
}
/** Cancel the rendering work of every diff in the list */
- _cancelDiffs() {
- if (this._cancelForEachDiff) {
- this._cancelForEachDiff();
+ private cancelDiffs() {
+ if (this.cancelForEachDiff) {
+ this.cancelForEachDiff();
}
- this._forEachDiff(d => d.cancel());
+ this.forEachDiff(d => d.cancel());
}
/**
* In the given NodeList of diff elements, find the diff for the given path.
*/
- private _findDiffByPath(path: string, diffElements: GrDiffHost[]) {
+ private findDiffByPath(path: string, diffElements: GrDiffHost[]) {
for (let i = 0; i < diffElements.length; i++) {
if (diffElements[i].path === path) {
return diffElements[i];
@@ -1430,8 +2307,9 @@
return undefined;
}
- _handleEscKey() {
- this._displayLine = false;
+ // Private but used in tests.
+ handleEscKey() {
+ this.displayLine = false;
}
/**
@@ -1439,27 +2317,24 @@
* debouncer so that the file list doesn't flash gray when the API requests
* are reasonably fast.
*/
- _loadingChanged(loading?: boolean) {
+ private loadingChanged() {
+ const loading = this.loading;
this.loadingTask = debounce(
this.loadingTask,
() => {
// Only show set the loading if there have been files loaded to show. In
// this way, the gray loading style is not shown on initial loads.
- this.classList.toggle('loading', loading && !!this._files.length);
+ this.classList.toggle('loading', loading && !!this.files.length);
},
LOADING_DEBOUNCE_INTERVAL
);
}
- _editModeChanged(editMode?: boolean) {
- this.classList.toggle('editMode', editMode);
- }
-
- _computeReviewedClass(isReviewed?: boolean) {
+ private computeReviewedClass(isReviewed?: boolean) {
return isReviewed ? 'isReviewed' : '';
}
- _computeReviewedText(isReviewed?: boolean) {
+ private computeReviewedText(isReviewed?: boolean) {
return isReviewed ? 'MARK UNREVIEWED' : 'MARK REVIEWED';
}
@@ -1467,7 +2342,7 @@
* Given a file path, return whether that path should have visible size bars
* and be included in the size bars calculation.
*/
- _showBarsForPath(path?: string) {
+ private showBarsForPath(path?: string) {
return (
path !== SpecialFilePath.COMMIT_MESSAGE &&
path !== SpecialFilePath.MERGE_LIST
@@ -1476,16 +2351,12 @@
/**
* Compute size bar layout values from the file list.
+ * Private but used in tests.
*/
- _computeSizeBarLayout(
- shownFilesRecord?: ElementPropertyDeepChange<GrFileList, '_shownFiles'>
- ) {
+ computeSizeBarLayout() {
const stats: SizeBarLayout = createDefaultSizeBarLayout();
- if (!shownFilesRecord || !shownFilesRecord.base) {
- return stats;
- }
- shownFilesRecord.base
- .filter(f => this._showBarsForPath(f.__path))
+ this.shownFiles
+ .filter(f => this.showBarsForPath(f.__path))
.forEach(f => {
if (f.lines_inserted) {
stats.maxInserted = Math.max(stats.maxInserted, f.lines_inserted);
@@ -1507,14 +2378,15 @@
/**
* Get the width of the addition bar for a file.
+ * Private but used in tests.
*/
- _computeBarAdditionWidth(file?: NormalizedFileInfo, stats?: SizeBarLayout) {
+ computeBarAdditionWidth(file?: NormalizedFileInfo, stats?: SizeBarLayout) {
if (
!file ||
!stats ||
stats.maxInserted === 0 ||
!file.lines_inserted ||
- !this._showBarsForPath(file.__path)
+ !this.showBarsForPath(file.__path)
) {
return 0;
}
@@ -1525,22 +2397,24 @@
/**
* Get the x-offset of the addition bar for a file.
+ * Private but used in tests.
*/
- _computeBarAdditionX(file?: NormalizedFileInfo, stats?: SizeBarLayout) {
+ computeBarAdditionX(file?: NormalizedFileInfo, stats?: SizeBarLayout) {
if (!file || !stats) return;
- return stats.maxAdditionWidth - this._computeBarAdditionWidth(file, stats);
+ return stats.maxAdditionWidth - this.computeBarAdditionWidth(file, stats);
}
/**
* Get the width of the deletion bar for a file.
+ * Private but used in tests.
*/
- _computeBarDeletionWidth(file?: NormalizedFileInfo, stats?: SizeBarLayout) {
+ computeBarDeletionWidth(file?: NormalizedFileInfo, stats?: SizeBarLayout) {
if (
!file ||
!stats ||
stats.maxDeleted === 0 ||
!file.lines_deleted ||
- !this._showBarsForPath(file.__path)
+ !this.showBarsForPath(file.__path)
) {
return 0;
}
@@ -1552,15 +2426,16 @@
/**
* Get the x-offset of the deletion bar for a file.
*/
- _computeBarDeletionX(stats: SizeBarLayout) {
+ private computeBarDeletionX(stats: SizeBarLayout) {
return stats.deletionOffset;
}
- _computeSizeBarsClass(showSizeBars?: boolean, path?: string) {
+ // Private but used in tests.
+ computeSizeBarsClass(path?: string) {
let hideClass = '';
- if (!showSizeBars) {
+ if (!this.showSizeBars) {
hideClass = 'hide';
- } else if (!this._showBarsForPath(path)) {
+ } else if (!this.showBarsForPath(path)) {
hideClass = 'invisible';
}
return `sizeBars ${hideClass}`;
@@ -1572,18 +2447,15 @@
* Ideally, there should be a better way to enforce the expectation of the
* dependencies between dynamic endpoints.
*/
- _computeShowDynamicColumns(
- headerEndpoints?: string,
- contentEndpoints?: string,
- summaryEndpoints?: string
- ) {
- return (
- headerEndpoints &&
- contentEndpoints &&
- summaryEndpoints &&
- headerEndpoints.length &&
- headerEndpoints.length === contentEndpoints.length &&
- headerEndpoints.length === summaryEndpoints.length
+ private computeShowDynamicColumns() {
+ return !!(
+ this.dynamicHeaderEndpoints &&
+ this.dynamicContentEndpoints &&
+ this.dynamicSummaryEndpoints &&
+ this.dynamicHeaderEndpoints.length &&
+ this.dynamicHeaderEndpoints.length ===
+ this.dynamicContentEndpoints.length &&
+ this.dynamicHeaderEndpoints.length === this.dynamicSummaryEndpoints.length
);
}
@@ -1591,22 +2463,21 @@
* Shows registered dynamic prepended columns iff the 'header', 'content'
* endpoints are registered the exact same number of times.
*/
- _computeShowPrependedDynamicColumns(
- headerEndpoints?: string,
- contentEndpoints?: string
- ) {
- return (
- headerEndpoints &&
- contentEndpoints &&
- headerEndpoints.length &&
- headerEndpoints.length === contentEndpoints.length
+ private computeShowPrependedDynamicColumns() {
+ return !!(
+ this.dynamicPrependedHeaderEndpoints &&
+ this.dynamicPrependedContentEndpoints &&
+ this.dynamicPrependedHeaderEndpoints.length &&
+ this.dynamicPrependedHeaderEndpoints.length ===
+ this.dynamicPrependedContentEndpoints.length
);
}
/**
* Returns true if none of the inline diffs have been expanded.
+ * Private but used in tests.
*/
- _noDiffsExpanded() {
+ noDiffsExpanded() {
return this.filesExpanded === FilesExpandedState.NONE;
}
@@ -1616,19 +2487,20 @@
* rendering.
*
* @param index The index of the row being rendered.
+ * Private but used in tests.
*/
- _reportRenderedRow(index: number) {
- if (index === this._shownFiles.length - 1) {
+ reportRenderedRow(index: number) {
+ if (index === this.shownFiles.length - 1) {
setTimeout(() => {
this.reporting.timeEnd(Timing.FILE_RENDER, {
- count: this._reportinShownFilesIncrement,
+ count: this.reportinShownFilesIncrement,
});
}, 1);
}
- return '';
}
- _reviewedTitle(reviewed?: boolean) {
+ // Private but used in tests.
+ reviewedTitle(reviewed?: boolean) {
if (reviewed) {
return 'Mark as not reviewed (shortcut: r)';
}
@@ -1636,25 +2508,11 @@
return 'Mark as reviewed (shortcut: r)';
}
- _handleReloadingDiffPreference() {
+ private handleReloadingDiffPreference() {
this.userModel.getDiffPreferences();
}
- /**
- * Wrapper for using in the element template and computed properties
- */
- _computeDisplayPath(path: string) {
- return computeDisplayPath(path);
- }
-
- /**
- * Wrapper for using in the element template and computed properties
- */
- _computeTruncatedPath(path: string) {
- return computeTruncatedPath(path);
- }
-
- _getOldPath(file: NormalizedFileInfo) {
+ private getOldPath(file: NormalizedFileInfo) {
// The gr-endpoint-decorator is waiting until all gr-endpoint-param
// values are updated.
// The old_path property is undefined for added files, and the
@@ -1664,9 +2522,3 @@
return file.old_path ?? null;
}
}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-file-list': GrFileList;
- }
-}
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
deleted file mode 100644
index 67ca9be..0000000
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.ts
+++ /dev/null
@@ -1,823 +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 {
- display: block;
- }
- .row {
- align-items: center;
- border-top: 1px solid var(--border-color);
- display: flex;
- min-height: calc(var(--line-height-normal) + 2 * var(--spacing-s));
- padding: var(--spacing-xs) var(--spacing-l);
- }
- /* The class defines a content visible only to screen readers */
- .noCommentsScreenReaderText {
- opacity: 0;
- max-width: 1px;
- overflow: hidden;
- display: none;
- vertical-align: top;
- }
- div[role='gridcell']
- > div.comments
- > span:empty
- + span:empty
- + span.noCommentsScreenReaderText {
- /* inline-block instead of block, such that it can control width */
- display: inline-block;
- }
- :host(.loading) .row {
- opacity: 0.5;
- }
- :host(.editMode) .hideOnEdit {
- display: none;
- }
- .showOnEdit {
- display: none;
- }
- :host(.editMode) .showOnEdit {
- display: initial;
- }
- .invisible {
- visibility: hidden;
- }
- .header-row {
- background-color: var(--background-color-secondary);
- }
- .controlRow {
- align-items: center;
- display: flex;
- height: 2.25em;
- justify-content: center;
- }
- .controlRow.invisible,
- .show-hide.invisible {
- display: none;
- }
- .reviewed,
- .status {
- align-items: center;
- display: inline-flex;
- }
- .reviewed {
- display: inline-block;
- text-align: left;
- width: 1.5em;
- }
- .file-row {
- cursor: pointer;
- }
- .file-row.expanded {
- border-bottom: 1px solid var(--border-color);
- position: -webkit-sticky;
- position: sticky;
- top: 0;
- /* Has to visible above the diff view, and by default has a lower
- z-index. setting to 1 places it directly above. */
- z-index: 1;
- }
- .file-row:hover {
- background-color: var(--hover-background-color);
- }
- .file-row.selected {
- background-color: var(--selection-background-color);
- }
- .file-row.expanded,
- .file-row.expanded:hover {
- background-color: var(--expanded-background-color);
- }
- .path {
- cursor: pointer;
- flex: 1;
- /* Wrap it into multiple lines if too long. */
- white-space: normal;
- word-break: break-word;
- }
- .oldPath {
- color: var(--deemphasized-text-color);
- }
- .header-stats {
- text-align: center;
- min-width: 7.5em;
- }
- .stats {
- text-align: right;
- min-width: 7.5em;
- }
- .comments {
- padding-left: var(--spacing-l);
- min-width: 7.5em;
- white-space: nowrap;
- }
- .row:not(.header-row) .stats,
- .total-stats {
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-mono);
- line-height: var(--line-height-mono);
- display: flex;
- }
- .sizeBars {
- margin-left: var(--spacing-m);
- min-width: 7em;
- text-align: center;
- }
- .sizeBars.hide {
- display: none;
- }
- .added,
- .removed {
- display: inline-block;
- min-width: 3.5em;
- }
- .added {
- color: var(--positive-green-text-color);
- }
- .removed {
- color: var(--negative-red-text-color);
- text-align: left;
- min-width: 4em;
- padding-left: var(--spacing-s);
- }
- .drafts {
- color: var(--error-foreground);
- font-weight: var(--font-weight-bold);
- }
- .show-hide-icon:focus {
- outline: none;
- }
- .show-hide {
- margin-left: var(--spacing-s);
- width: 1.9em;
- }
- .fileListButton {
- margin: var(--spacing-m);
- }
- .totalChanges {
- justify-content: flex-end;
- text-align: right;
- }
- .warning {
- color: var(--deemphasized-text-color);
- }
- input.show-hide {
- display: none;
- }
- label.show-hide {
- cursor: pointer;
- display: block;
- min-width: 2em;
- }
- gr-diff {
- display: block;
- overflow-x: auto;
- }
- .truncatedFileName {
- display: none;
- }
- .mobile {
- display: none;
- }
- .reviewed {
- margin-left: var(--spacing-xxl);
- width: 15em;
- }
- .reviewedSwitch {
- color: var(--link-color);
- opacity: 0;
- justify-content: flex-end;
- width: 100%;
- }
- .reviewedSwitch:hover {
- cursor: pointer;
- opacity: 100;
- }
- .showParentButton {
- line-height: var(--line-height-normal);
- margin-bottom: calc(var(--spacing-s) * -1);
- margin-left: var(--spacing-m);
- margin-top: calc(var(--spacing-s) * -1);
- }
- .row:focus {
- outline: none;
- }
- .row:hover .reviewedSwitch,
- .row:focus-within .reviewedSwitch,
- .row.expanded .reviewedSwitch {
- opacity: 100;
- }
- .reviewedLabel {
- color: var(--deemphasized-text-color);
- margin-right: var(--spacing-l);
- opacity: 0;
- }
- .reviewedLabel.isReviewed {
- display: initial;
- opacity: 100;
- }
- .editFileControls {
- width: 7em;
- }
- .markReviewed:focus {
- outline: none;
- }
- .markReviewed,
- .pathLink {
- display: inline-block;
- margin: -2px 0;
- padding: var(--spacing-s) 0;
- text-decoration: none;
- }
- .pathLink:hover span.fullFileName,
- .pathLink:hover span.truncatedFileName {
- text-decoration: underline;
- }
-
- /** copy on file path **/
- .pathLink gr-copy-clipboard,
- .oldPath gr-copy-clipboard {
- display: inline-block;
- visibility: hidden;
- vertical-align: bottom;
- --gr-button-padding: 0px;
- }
- .row:focus-within gr-copy-clipboard,
- .row:hover gr-copy-clipboard {
- visibility: visible;
- }
-
- @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;
- }
- .mobile {
- display: block;
- }
- .row.selected {
- background-color: var(--view-background-color);
- }
- .stats {
- display: none;
- }
- .reviewed,
- .status {
- justify-content: flex-start;
- }
- .comments {
- min-width: initial;
- }
- .expanded .fullFileName,
- .truncatedFileName {
- display: inline;
- }
- .expanded .truncatedFileName,
- .fullFileName {
- display: none;
- }
- }
- :host(.hideComments) {
- --gr-comment-thread-display: none;
- }
- </style>
- <h3 class="assistive-tech-only">File list</h3>
- <div
- id="container"
- on-click="_handleFileListClick"
- role="grid"
- aria-label="Files list"
- >
- <div class="header-row row" role="row">
- <!-- endpoint: change-view-file-list-header-prepend -->
- <template is="dom-if" if="[[_showPrependedDynamicColumns]]">
- <template
- is="dom-repeat"
- items="[[_dynamicPrependedHeaderEndpoints]]"
- as="headerEndpoint"
- >
- <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]]">
- </gr-endpoint-param>
- <gr-endpoint-param name="files" value="[[_files]]">
- </gr-endpoint-param>
- </gr-endpoint-decorator>
- </template>
- </template>
- <div class="path" role="columnheader">File</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]]">
- <template
- is="dom-repeat"
- items="[[_dynamicHeaderEndpoints]]"
- as="headerEndpoint"
- >
- <gr-endpoint-decorator
- class="extra-col"
- name$="[[headerEndpoint]]"
- role="columnheader"
- >
- </gr-endpoint-decorator>
- </template>
- </template>
- <!-- Empty div here exists to keep spacing in sync with file rows. -->
- <div
- class="reviewed hideOnEdit"
- hidden$="[[!_loggedIn]]"
- aria-hidden="true"
- ></div>
- <div class="editFileControls showOnEdit" aria-hidden="true"></div>
- <div class="show-hide" aria-hidden="true"></div>
- </div>
-
- <template
- is="dom-repeat"
- items="[[_shownFiles]]"
- id="files"
- as="file"
- initial-count="[[fileListIncrement]]"
- target-framerate="1"
- >
- [[_reportRenderedRow(index)]]
- <div class="stickyArea">
- <div
- class$="file-row row [[_computePathClass(file.__path, _expandedFiles.*)]]"
- data-file$="[[_computePatchSetFile(file)]]"
- tabindex="-1"
- role="row"
- >
- <!-- endpoint: change-view-file-list-content-prepend -->
- <template is="dom-if" if="[[_showPrependedDynamicColumns]]">
- <template
- is="dom-repeat"
- items="[[_dynamicPrependedContentEndpoints]]"
- as="contentEndpoint"
- >
- <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]]">
- </gr-endpoint-param>
- <gr-endpoint-param name="patchRange" value="[[patchRange]]">
- </gr-endpoint-param>
- <gr-endpoint-param name="path" value="[[file.__path]]">
- </gr-endpoint-param>
- <gr-endpoint-param name="oldPath" value="[[_getOldPath(file)]]">
- </gr-endpoint-param>
- </gr-endpoint-decorator>
- </template>
- </template>
- <!-- TODO: Remove data-url as it appears its not used -->
- <span
- data-url="[[_computeDiffURL(change, patchRange.basePatchNum, patchRange.patchNum, file.__path, editMode)]]"
- class="path"
- role="gridcell"
- >
- <a
- class="pathLink"
- href$="[[_computeDiffURL(change, patchRange.basePatchNum, patchRange.patchNum, file.__path, editMode)]]"
- >
- <span
- title$="[[_computeDisplayPath(file.__path)]]"
- class="fullFileName"
- >
- [[_computeDisplayPath(file.__path)]]
- </span>
- <span
- title$="[[_computeDisplayPath(file.__path)]]"
- class="truncatedFileName"
- >
- [[_computeTruncatedPath(file.__path)]]
- </span>
- <gr-file-status-chip file="[[file]]"></gr-file-status-chip>
- <gr-copy-clipboard
- hideInput=""
- text="[[file.__path]]"
- ></gr-copy-clipboard>
- </a>
- <template is="dom-if" if="[[file.old_path]]">
- <div class="oldPath" title$="[[file.old_path]]">
- [[file.old_path]]
- <gr-copy-clipboard
- hideInput=""
- text="[[file.old_path]]"
- ></gr-copy-clipboard>
- </div>
- </template>
- </span>
- <div role="gridcell">
- <div class="comments desktop">
- <span class="drafts"
- ><!-- This comments ensure that span is empty when the function
- returns empty string.
- -->[[_computeDraftsString(changeComments, patchRange, file)]]<!-- This comments ensure that span is empty when
- the function returns empty string.
- --></span
- >
- <span
- ><!--
- -->[[_computeCommentsString(changeComments, patchRange, file)]]<!--
- --></span
- >
- <span class="noCommentsScreenReaderText">
- <!-- Screen readers read the following content only if 2 other
- spans in the parent div is empty. The content is not visible on
- the page.
- Without this span, screen readers don't navigate correctly inside
- table, because empty div doesn't rendered. For example, VoiceOver
- jumps back to the whole table.
- We can use   instead, but it sounds worse.
- -->
- No comments
- </span>
- </div>
- <div class="comments mobile">
- <span class="drafts"
- ><!-- This comments ensure that span is empty when the function
- returns empty string.
- -->[[_computeDraftsStringMobile(changeComments, patchRange,
- file)]]<!-- This comments ensure that span is empty when
- the function returns empty string.
- --></span
- >
- <span
- ><!--
- -->[[_computeCommentsStringMobile(changeComments, patchRange,
- file)]]<!--
- --></span
- >
- <span class="noCommentsScreenReaderText">
- <!-- The same as for desktop comments -->
- No comments
- </span>
- </div>
- </div>
- <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
- "Commit message" row content with incorrect column headers.
- -->
- <div
- class$="[[_computeSizeBarsClass(_showSizeBars, file.__path)]]"
- aria-label="A bar that represents the addition and deletion ratio for the current file"
- >
- <svg width="61" height="8">
- <rect
- x$="[[_computeBarAdditionX(file, _sizeBarLayout)]]"
- y="0"
- height="8"
- fill="var(--positive-green-text-color)"
- width$="[[_computeBarAdditionWidth(file, _sizeBarLayout)]]"
- ></rect>
- <rect
- x$="[[_computeBarDeletionX(_sizeBarLayout)]]"
- y="0"
- height="8"
- fill="var(--negative-red-text-color)"
- width$="[[_computeBarDeletionWidth(file, _sizeBarLayout)]]"
- ></rect>
- </svg>
- </div>
- </div>
- <div class="stats" 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
- "Commit message" row content with incorrect column headers.
- -->
- <div class$="[[_computeClass('', file.__path)]]">
- <span
- class="added"
- tabindex="0"
- aria-label$="[[file.lines_inserted]] lines added"
- hidden$="[[file.binary]]"
- >
- +[[file.lines_inserted]]
- </span>
- <span
- class="removed"
- tabindex="0"
- aria-label$="[[file.lines_deleted]] lines removed"
- hidden$="[[file.binary]]"
- >
- -[[file.lines_deleted]]
- </span>
- <span
- class$="[[_computeBinaryClass(file.size_delta)]]"
- hidden$="[[!file.binary]]"
- >
- [[_formatBytes(file.size_delta)]] [[_formatPercentage(file.size,
- file.size_delta)]]
- </span>
- </div>
- </div>
- <!-- endpoint: change-view-file-list-content -->
- <template is="dom-if" if="[[_showDynamicColumns]]">
- <template
- is="dom-repeat"
- items="[[_dynamicContentEndpoints]]"
- as="contentEndpoint"
- >
- <div class$="[[_computeClass('', file.__path)]]" role="gridcell">
- <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]]">
- </gr-endpoint-param>
- <gr-endpoint-param name="patchRange" value="[[patchRange]]">
- </gr-endpoint-param>
- <gr-endpoint-param name="path" value="[[file.__path]]">
- </gr-endpoint-param>
- </gr-endpoint-decorator>
- </div>
- </template>
- </template>
- <div
- class="reviewed hideOnEdit"
- role="gridcell"
- hidden$="[[!_loggedIn]]"
- >
- <span
- class$="reviewedLabel [[_computeReviewedClass(file.isReviewed)]]"
- aria-hidden$="[[!file.isReviewed]]"
- >Reviewed</span
- >
- <!-- Do not use input type="checkbox" with hidden input and
- visible label here. Screen readers don't read/interract
- correctly with such input.
- -->
- <span
- class="reviewedSwitch"
- role="switch"
- tabindex="0"
- on-click="_reviewedClick"
- on-keydown="_reviewedClick"
- aria-label="Reviewed"
- aria-checked$="[[_booleanToString(file.isReviewed)]]"
- >
- <!-- Trick with tabindex to avoid outline on mouse focus, but
- preserve focus outline for keyboard navigation -->
- <span
- tabindex="-1"
- class="markReviewed"
- title$="[[_reviewedTitle(file.isReviewed)]]"
- >[[_computeReviewedText(file.isReviewed)]]</span
- >
- </span>
- </div>
- <div
- class="editFileControls showOnEdit"
- role="gridcell"
- aria-hidden$="[[!editMode]]"
- >
- <template is="dom-if" if="[[editMode]]">
- <gr-edit-file-controls
- class$="[[_computeClass('', file.__path)]]"
- file-path="[[file.__path]]"
- ></gr-edit-file-controls>
- </template>
- </div>
- <div class="show-hide" role="gridcell">
- <!-- Do not use input type="checkbox" with hidden input and
- visible label here. Screen readers don't read/interract
- correctly with such input.
- -->
- <span
- class="show-hide"
- data-path$="[[file.__path]]"
- data-expand="true"
- role="switch"
- tabindex="0"
- aria-checked$="[[_isFileExpandedStr(file.__path, _expandedFiles.*)]]"
- aria-label="Expand file"
- on-click="_expandedClick"
- on-keydown="_expandedClick"
- >
- <!-- Trick with tabindex to avoid outline on mouse focus, but
- preserve focus outline for keyboard navigation -->
- <iron-icon
- class="show-hide-icon"
- tabindex="-1"
- id="icon"
- icon="[[_computeShowHideIcon(file.__path, _expandedFiles.*)]]"
- >
- </iron-icon>
- </span>
- </div>
- </div>
- <template
- is="dom-if"
- if="[[_isFileExpanded(file.__path, _expandedFiles.*)]]"
- >
- <gr-diff-host
- no-auto-render=""
- show-load-failure=""
- display-line="[[_displayLine]]"
- hidden="[[!_isFileExpanded(file.__path, _expandedFiles.*)]]"
- change-num="[[changeNum]]"
- change="[[change]]"
- patch-range="[[patchRange]]"
- file="[[_computePatchSetFile(file)]]"
- path="[[file.__path]]"
- prefs="[[diffPrefs]]"
- project-name="[[change.project]]"
- no-render-on-prefs-change=""
- ></gr-diff-host>
- </template>
- </div>
- </template>
- <template
- is="dom-if"
- if="[[_computeShowNumCleanlyMerged(_cleanlyMergedPaths)]]"
- >
- <div class="row">
- <!-- endpoint: change-view-file-list-content-prepend -->
- <template is="dom-if" if="[[_showPrependedDynamicColumns]]">
- <template
- is="dom-repeat"
- items="[[_dynamicPrependedContentEndpoints]]"
- as="contentEndpoint"
- >
- <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]]">
- </gr-endpoint-param>
- <gr-endpoint-param name="patchRange" value="[[patchRange]]">
- </gr-endpoint-param>
- <gr-endpoint-param
- name="cleanlyMergedPaths"
- value="[[_cleanlyMergedPaths]]"
- >
- </gr-endpoint-param>
- <gr-endpoint-param
- name="cleanlyMergedOldPaths"
- value="[[_cleanlyMergedOldPaths]]"
- >
- </gr-endpoint-param>
- </gr-endpoint-decorator>
- </template>
- </template>
- <div role="gridcell">
- <div>
- <span class="cleanlyMergedText">
- [[_computeCleanlyMergedText(_cleanlyMergedPaths)]]
- </span>
- <gr-button
- link
- class="showParentButton"
- on-click="_handleShowParent1"
- >
- Show Parent 1
- </gr-button>
- </div>
- </div>
- </div>
- </template>
- </div>
- <div class="row totalChanges" hidden$="[[_hideChangeTotals]]">
- <div class="total-stats">
- <div>
- <span
- class="added"
- tabindex="0"
- aria-label$="Total [[_patchChange.inserted]] lines added"
- >
- +[[_patchChange.inserted]]
- </span>
- <span
- class="removed"
- tabindex="0"
- aria-label$="Total [[_patchChange.deleted]] lines removed"
- >
- -[[_patchChange.deleted]]
- </span>
- </div>
- </div>
- <!-- endpoint: change-view-file-list-summary -->
- <template is="dom-if" if="[[_showDynamicColumns]]">
- <template
- is="dom-repeat"
- items="[[_dynamicSummaryEndpoints]]"
- as="summaryEndpoint"
- >
- <gr-endpoint-decorator class="extra-col" name="[[summaryEndpoint]]">
- <gr-endpoint-param
- name="change"
- value="[[change]]"
- ></gr-endpoint-param>
- <gr-endpoint-param name="patchRange" value="[[patchRange]]">
- </gr-endpoint-param>
- </gr-endpoint-decorator>
- </template>
- </template>
- <!-- Empty div here exists to keep spacing in sync with file rows. -->
- <div class="reviewed hideOnEdit" hidden$="[[!_loggedIn]]"></div>
- <div class="editFileControls showOnEdit"></div>
- <div class="show-hide"></div>
- </div>
- <div class="row totalChanges" hidden$="[[_hideBinaryChangeTotals]]">
- <div class="total-stats">
- <span
- class="added"
- aria-label$="Total bytes inserted: [[_formatBytes(_patchChange.size_delta_inserted)]] "
- >
- [[_formatBytes(_patchChange.size_delta_inserted)]]
- [[_formatPercentage(_patchChange.total_size,
- _patchChange.size_delta_inserted)]]
- </span>
- <span
- class="removed"
- aria-label$="Total bytes removed: [[_formatBytes(_patchChange.size_delta_deleted)]]"
- >
- [[_formatBytes(_patchChange.size_delta_deleted)]]
- [[_formatPercentage(_patchChange.total_size,
- _patchChange.size_delta_deleted)]]
- </span>
- </div>
- </div>
- <div
- class$="row controlRow [[_computeFileListControlClass(numFilesShown, _files)]]"
- >
- <gr-button
- class="fileListButton"
- id="incrementButton"
- link=""
- on-click="_incrementNumFilesShown"
- >
- [[_computeIncrementText(numFilesShown, _files)]]
- </gr-button>
- <gr-tooltip-content
- has-tooltip="[[_computeWarnShowAll(_files)]]"
- show-icon="[[_computeWarnShowAll(_files)]]"
- title$="[[_computeShowAllWarning(_files)]]"
- >
- <gr-button
- class="fileListButton"
- id="showAllButton"
- link=""
- on-click="_showAllFiles"
- >
- [[_computeShowAllText(_files)]] </gr-button
- ><!--
- --></gr-tooltip-content>
- </div>
- <gr-diff-preferences-dialog
- id="diffPreferencesDialog"
- diff-prefs="{{diffPrefs}}"
- on-reload-diff-preference="_handleReloadingDiffPreference"
- >
- </gr-diff-preferences-dialog>
-`;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
index 8d82e41..d39d648 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
@@ -17,16 +17,13 @@
import '../../../test/common-test-setup-karma';
import '../../shared/gr-date-formatter/gr-date-formatter';
import './gr-file-list';
-import {createCommentApiMockWithTemplateElement} from '../../../test/mocks/comment-api';
import {FilesExpandedState} from '../gr-file-list-constants';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {runA11yAudit} from '../../../test/a11y-test-utils';
-import {html} from '@polymer/polymer/lib/utils/html-tag';
import {
listenOnce,
mockPromise,
query,
- spyRestApi,
stubRestApi,
} from '../../../test/test-utils';
import {
@@ -34,6 +31,7 @@
CommitId,
EditPatchSetNum,
NumericChangeId,
+ ParentPatchSetNum,
PatchRange,
PatchSetNum,
RepoName,
@@ -49,24 +47,20 @@
createParsedChange,
createRevision,
} from '../../../test/test-data-generators';
-import {createDefaultDiffPrefs} from '../../../constants/constants';
+import {
+ createDefaultDiffPrefs,
+ DiffViewMode,
+} from '../../../constants/constants';
import {queryAll, queryAndAssert} from '../../../utils/common-util';
import {GrFileList, NormalizedFileInfo} from './gr-file-list';
-import {DiffPreferencesInfo} from '../../../types/diff';
import {GrButton} from '../../shared/gr-button/gr-button';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import {ParsedChangeInfo} from '../../../types/types';
import {GrDiffHost} from '../../diff/gr-diff-host/gr-diff-host';
import {IronIconElement} from '@polymer/iron-icon';
-import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
import {GrEditFileControls} from '../../edit/gr-edit-file-controls/gr-edit-file-controls';
-const commentApiMock = createCommentApiMockWithTemplateElement(
- 'gr-file-list-comment-api-mock',
- html` <gr-file-list id="fileList"></gr-file-list> `
-);
-
-const basicFixture = fixtureFromElement(commentApiMock.is);
+const basicFixture = fixtureFromElement('gr-file-list');
suite('gr-diff a11y test', () => {
test('audit', async () => {
@@ -77,19 +71,18 @@
function createFilesByPath(count: number) {
return Array(count)
.fill(0)
- .reduce((_filesByPath, _, idx) => {
- _filesByPath[`'/file${idx}`] = {lines_inserted: 9};
- return _filesByPath;
+ .reduce((filesByPath, _, idx) => {
+ filesByPath[`'/file${idx}`] = {lines_inserted: 9};
+ return filesByPath;
}, {});
}
suite('gr-file-list tests', () => {
let element: GrFileList;
- let commentApiWrapper: any;
let saveStub: sinon.SinonStub;
- suite('basic tests', () => {
+ suite('basic tests', async () => {
setup(async () => {
stubRestApi('getDiffComments').returns(Promise.resolve({}));
stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
@@ -101,13 +94,22 @@
stub('gr-diff-host', 'reload').callsFake(() => Promise.resolve());
stub('gr-diff-host', 'prefetchDiff').callsFake(() => {});
- // Element must be wrapped in an element with direct access to the
- // comment API.
- commentApiWrapper = basicFixture.instantiate();
- element = commentApiWrapper.$.fileList;
+ element = basicFixture.instantiate();
- element._loading = false;
- element.diffPrefs = {} as DiffPreferencesInfo;
+ element.loading = false;
+ element.diffPrefs = {
+ context: 10,
+ tab_size: 8,
+ font_size: 12,
+ line_length: 100,
+ cursor_blink_rate: 0,
+ line_wrapping: false,
+ show_line_endings: true,
+ show_tabs: true,
+ show_whitespace_errors: true,
+ syntax_highlighting: true,
+ ignore_whitespace: 'IGNORE_NONE',
+ };
element.numFilesShown = 200;
element.patchRange = {
basePatchNum: 'PARENT' as BasePatchSetNum,
@@ -116,6 +118,9 @@
saveStub = sinon
.stub(element, '_saveReviewedState')
.callsFake(() => Promise.resolve());
+ await element.updateComplete;
+ // Wait for expandedFilesChanged to complete.
+ await flush();
});
test('renders', () => {
@@ -126,9 +131,6 @@
</h3>
<div aria-label="Files list" id="container" role="grid">
<div class="header-row row" role="row">
- <dom-if style="display: none;">
- <template is="dom-if"> </template>
- </dom-if>
<div class="path" role="columnheader">File</div>
<div class="comments desktop" role="columnheader">Comments</div>
<div class="comments mobile" role="columnheader" title="Comments">
@@ -136,60 +138,10 @@
</div>
<div class="desktop sizeBars" role="columnheader">Size</div>
<div class="header-stats" role="columnheader">Delta</div>
- <dom-if style="display: none;">
- <template is="dom-if"> </template>
- </dom-if>
- <div
- aria-hidden="true"
- class="hideOnEdit reviewed"
- hidden="true"
- ></div>
+ <div aria-hidden="true" class="hideOnEdit reviewed" hidden=""></div>
<div aria-hidden="true" class="editFileControls showOnEdit"></div>
<div aria-hidden="true" class="show-hide"></div>
</div>
- <dom-repeat
- as="file"
- id="files"
- style="display: none;"
- target-framerate="1"
- >
- <template is="dom-repeat"> </template>
- </dom-repeat>
- <dom-if style="display: none;">
- <template is="dom-if"> </template>
- </dom-if>
- </div>
- <div class="row totalChanges" hidden="true">
- <div class="total-stats">
- <div>
- <span aria-label="Total 0 lines added" class="added" tabindex="0">
- +0
- </span>
- <span
- aria-label="Total 0 lines removed"
- class="removed"
- tabindex="0"
- >
- -0
- </span>
- </div>
- </div>
- <dom-if style="display: none;">
- <template is="dom-if"> </template>
- </dom-if>
- <div class="hideOnEdit reviewed" hidden="true"></div>
- <div class="editFileControls showOnEdit"></div>
- <div class="show-hide"></div>
- </div>
- <div class="row totalChanges" hidden="true">
- <div class="total-stats">
- <span aria-label="Total bytes inserted: +/-0 B " class="added">
- +/-0 B
- </span>
- <span aria-label="Total bytes removed: +/-0 B" class="removed">
- +/-0 B
- </span>
- </div>
</div>
<div class="controlRow invisible row">
<gr-button
@@ -220,9 +172,9 @@
></gr-diff-preferences-dialog>`);
});
- test('renders file row', () => {
- element._filesByPath = createFilesByPath(1);
- flush();
+ test('renders file row', async () => {
+ element.filesByPath = createFilesByPath(1);
+ await element.updateComplete;
const fileRows = queryAll<HTMLDivElement>(element, '.file-row');
expect(fileRows?.[0]).dom.equal(/* HTML */ `<div
class="file-row row"
@@ -230,9 +182,6 @@
role="row"
tabindex="-1"
>
- <dom-if style="display: none;">
- <template is="dom-if"> </template>
- </dom-if>
<span class="path" role="gridcell">
<a class="pathLink">
<span class="fullFileName" title="'/file0"> '/file0 </span>
@@ -240,9 +189,6 @@
<gr-file-status-chip> </gr-file-status-chip>
<gr-copy-clipboard hideinput=""> </gr-copy-clipboard>
</a>
- <dom-if style="display: none;">
- <template is="dom-if"> </template>
- </dom-if>
</span>
<div role="gridcell">
<div class="comments desktop">
@@ -257,7 +203,7 @@
<div class="desktop" role="gridcell">
<div
aria-label="A bar that represents the addition and deletion ratio for the current file"
- class="sizeBars"
+ class="hide sizeBars"
></div>
</div>
<div class="stats" role="gridcell">
@@ -268,35 +214,14 @@
<span aria-label="0 lines removed" class="removed" tabindex="0">
-0
</span>
- <span hidden="true"> +/-0 B </span>
+ <span hidden=""> +/-0 B </span>
</div>
</div>
- <dom-if style="display: none;">
- <template is="dom-if"> </template>
- </dom-if>
- <div class="hideOnEdit reviewed" hidden="true" role="gridcell">
- <span aria-hidden="true" class="reviewedLabel"> Reviewed </span>
- <span
- aria-checked="false"
- aria-label="Reviewed"
- class="reviewedSwitch"
- role="switch"
- tabindex="0"
- >
- <span
- class="markReviewed"
- tabindex="-1"
- title="Mark as reviewed (shortcut: r)"
- >
- MARK REVIEWED
- </span>
- </span>
- </div>
- <div class="editFileControls showOnEdit" role="gridcell">
- <dom-if style="display: none;">
- <template is="dom-if"> </template>
- </dom-if>
- </div>
+ <div
+ aria-hidden="true"
+ class="editFileControls showOnEdit"
+ role="gridcell"
+ ></div>
<div class="show-hide" role="gridcell">
<span
aria-checked="false"
@@ -307,18 +232,24 @@
role="switch"
tabindex="0"
>
- <iron-icon class="show-hide-icon" id="icon" tabindex="-1">
+ <iron-icon
+ class="show-hide-icon"
+ id="icon"
+ tabindex="-1"
+ icon="gr-icons:expand-more"
+ >
</iron-icon>
</span>
</div>
</div>`);
});
- test('correct number of files are shown', () => {
+ test('correct number of files are shown', async () => {
element.fileListIncrement = 300;
- element._filesByPath = createFilesByPath(500);
+ element.filesByPath = createFilesByPath(500);
+ await element.updateComplete;
+ await flush();
- flush();
assert.equal(
queryAll<HTMLDivElement>(element, '.file-row').length,
element.numFilesShown
@@ -338,22 +269,25 @@
);
MockInteractions.tap(queryAndAssert<GrButton>(element, '#showAllButton'));
- flush();
+ await element.updateComplete;
+ await flush();
assert.equal(element.numFilesShown, 500);
- assert.equal(element._shownFiles.length, 500);
+ assert.equal(element.shownFiles.length, 500);
assert.isTrue(controlRow.classList.contains('invisible'));
});
- test('rendering each row calls the _reportRenderedRow method', () => {
- const renderedStub = sinon.stub(element, '_reportRenderedRow');
- element._filesByPath = createFilesByPath(10);
+ test('rendering each row calls the reportRenderedRow method', async () => {
+ const renderedStub = sinon.stub(element, 'reportRenderedRow');
+ element.filesByPath = createFilesByPath(10);
+ await element.updateComplete;
+
assert.equal(queryAll<HTMLDivElement>(element, '.file-row').length, 10);
assert.equal(renderedStub.callCount, 10);
});
- test('calculate totals for patch number', () => {
- element._filesByPath = {
+ test('calculate totals for patch number', async () => {
+ element.filesByPath = {
'/COMMIT_MSG': {
lines_inserted: 9,
size: 0,
@@ -377,19 +311,21 @@
size: 100,
},
};
+ await element.updateComplete;
- assert.deepEqual(element._patchChange, {
+ let patchChange = element.calculatePatchChange();
+ assert.deepEqual(patchChange, {
inserted: 2,
deleted: 2,
size_delta_inserted: 0,
size_delta_deleted: 0,
total_size: 0,
});
- assert.isTrue(element._hideBinaryChangeTotals);
- assert.isFalse(element._hideChangeTotals);
+ assert.isTrue(element.shouldHideBinaryChangeTotals(patchChange));
+ assert.isFalse(element.shouldHideChangeTotals(patchChange));
// Test with a commit message that isn't the first file.
- element._filesByPath = {
+ element.filesByPath = {
'file_added_in_rev2.txt': {
lines_inserted: 1,
lines_deleted: 1,
@@ -413,19 +349,21 @@
size_delta: 0,
},
};
+ await element.updateComplete;
- assert.deepEqual(element._patchChange, {
+ patchChange = element.calculatePatchChange();
+ assert.deepEqual(patchChange, {
inserted: 2,
deleted: 2,
size_delta_inserted: 0,
size_delta_deleted: 0,
total_size: 0,
});
- assert.isTrue(element._hideBinaryChangeTotals);
- assert.isFalse(element._hideChangeTotals);
+ assert.isTrue(element.shouldHideBinaryChangeTotals(patchChange));
+ assert.isFalse(element.shouldHideChangeTotals(patchChange));
// Test with no commit message.
- element._filesByPath = {
+ element.filesByPath = {
'file_added_in_rev2.txt': {
lines_inserted: 1,
lines_deleted: 1,
@@ -439,19 +377,21 @@
size_delta: 0,
},
};
+ await element.updateComplete;
- assert.deepEqual(element._patchChange, {
+ patchChange = element.calculatePatchChange();
+ assert.deepEqual(patchChange, {
inserted: 2,
deleted: 2,
size_delta_inserted: 0,
size_delta_deleted: 0,
total_size: 0,
});
- assert.isTrue(element._hideBinaryChangeTotals);
- assert.isFalse(element._hideChangeTotals);
+ assert.isTrue(element.shouldHideBinaryChangeTotals(patchChange));
+ assert.isFalse(element.shouldHideChangeTotals(patchChange));
// Test with files missing either lines_inserted or lines_deleted.
- element._filesByPath = {
+ element.filesByPath = {
'file_added_in_rev2.txt': {
lines_inserted: 1,
size: 0,
@@ -463,19 +403,22 @@
size_delta: 0,
},
};
- assert.deepEqual(element._patchChange, {
+ await element.updateComplete;
+
+ patchChange = element.calculatePatchChange();
+ assert.deepEqual(patchChange, {
inserted: 1,
deleted: 1,
size_delta_inserted: 0,
size_delta_deleted: 0,
total_size: 0,
});
- assert.isTrue(element._hideBinaryChangeTotals);
- assert.isFalse(element._hideChangeTotals);
+ assert.isTrue(element.shouldHideBinaryChangeTotals(patchChange));
+ assert.isFalse(element.shouldHideChangeTotals(patchChange));
});
- test('binary only files', () => {
- element._filesByPath = {
+ test('binary only files', async () => {
+ element.filesByPath = {
'/COMMIT_MSG': {
lines_inserted: 9,
size: 0,
@@ -484,19 +427,22 @@
file_binary_1: {binary: true, size_delta: 10, size: 100},
file_binary_2: {binary: true, size_delta: -5, size: 120},
};
- assert.deepEqual(element._patchChange, {
+ await element.updateComplete;
+
+ const patchChange = element.calculatePatchChange();
+ assert.deepEqual(patchChange, {
inserted: 0,
deleted: 0,
size_delta_inserted: 10,
size_delta_deleted: -5,
total_size: 220,
});
- assert.isFalse(element._hideBinaryChangeTotals);
- assert.isTrue(element._hideChangeTotals);
+ assert.isFalse(element.shouldHideBinaryChangeTotals(patchChange));
+ assert.isTrue(element.shouldHideChangeTotals(patchChange));
});
- test('binary and regular files', () => {
- element._filesByPath = {
+ test('binary and regular files', async () => {
+ element.filesByPath = {
'/COMMIT_MSG': {
lines_inserted: 9,
size: 0,
@@ -511,18 +457,21 @@
size_delta: 0,
},
};
- assert.deepEqual(element._patchChange, {
+ await element.updateComplete;
+
+ const patchChange = element.calculatePatchChange();
+ assert.deepEqual(patchChange, {
inserted: 10,
deleted: 5,
size_delta_inserted: 10,
size_delta_deleted: -5,
total_size: 220,
});
- assert.isFalse(element._hideBinaryChangeTotals);
- assert.isFalse(element._hideChangeTotals);
+ assert.isFalse(element.shouldHideBinaryChangeTotals(patchChange));
+ assert.isFalse(element.shouldHideChangeTotals(patchChange));
});
- test('_formatBytes function', () => {
+ test('formatBytes function', () => {
const table = {
'64': '+64 B',
'1023': '+1023 B',
@@ -537,11 +486,11 @@
'0': '+/-0 B',
};
for (const [bytes, expected] of Object.entries(table)) {
- assert.equal(element._formatBytes(Number(bytes)), expected);
+ assert.equal(element.formatBytes(Number(bytes)), expected);
}
});
- test('_formatPercentage function', () => {
+ test('formatPercentage function', () => {
const table = [
{size: 100, delta: 100, display: ''},
{size: 195060, delta: 64, display: '(+0%)'},
@@ -553,7 +502,7 @@
for (const item of table) {
assert.equal(
- element._formatPercentage(item.size, item.delta),
+ element.formatPercentage(item.size, item.delta),
item.display
);
}
@@ -576,224 +525,253 @@
patchNum: 2 as RevisionPatchSetNum,
};
+ element.patchRange = parentTo1;
assert.equal(
- element._computeCommentsStringMobile(
- element.changeComments,
- parentTo1,
- {__path: '/COMMIT_MSG', size: 0, size_delta: 0}
- ),
+ element.computeCommentsStringMobile({
+ __path: '/COMMIT_MSG',
+ size: 0,
+ size_delta: 0,
+ }),
'2c'
);
+ element.patchRange = _1To2;
assert.equal(
- element._computeCommentsStringMobile(element.changeComments, _1To2, {
+ element.computeCommentsStringMobile({
__path: '/COMMIT_MSG',
size: 0,
size_delta: 0,
}),
'3c'
);
+ element.patchRange = parentTo1;
assert.equal(
- element._computeDraftsString(element.changeComments, parentTo1, {
+ element.computeDraftsString({
__path: 'unresolved.file',
size: 0,
size_delta: 0,
}),
'1 draft'
);
+ element.patchRange = _1To2;
assert.equal(
- element._computeDraftsString(element.changeComments, _1To2, {
+ element.computeDraftsString({
__path: 'unresolved.file',
size: 0,
size_delta: 0,
}),
'1 draft'
);
+ element.patchRange = parentTo1;
assert.equal(
- element._computeDraftsStringMobile(element.changeComments, parentTo1, {
+ element.computeDraftsStringMobile({
__path: 'unresolved.file',
size: 0,
size_delta: 0,
}),
'1d'
);
+ element.patchRange = _1To2;
assert.equal(
- element._computeDraftsStringMobile(element.changeComments, _1To2, {
+ element.computeDraftsStringMobile({
__path: 'unresolved.file',
size: 0,
size_delta: 0,
}),
'1d'
);
+ element.patchRange = parentTo1;
assert.equal(
- element._computeCommentsStringMobile(
- element.changeComments,
- parentTo1,
- {__path: 'myfile.txt', size: 0, size_delta: 0}
- ),
+ element.computeCommentsStringMobile({
+ __path: 'myfile.txt',
+ size: 0,
+ size_delta: 0,
+ }),
'1c'
);
+ element.patchRange = _1To2;
assert.equal(
- element._computeCommentsStringMobile(element.changeComments, _1To2, {
+ element.computeCommentsStringMobile({
__path: 'myfile.txt',
size: 0,
size_delta: 0,
}),
'3c'
);
+ element.patchRange = parentTo1;
assert.equal(
- element._computeDraftsString(element.changeComments, parentTo1, {
+ element.computeDraftsString({
__path: 'myfile.txt',
size: 0,
size_delta: 0,
}),
''
);
+ element.patchRange = _1To2;
assert.equal(
- element._computeDraftsString(element.changeComments, _1To2, {
+ element.computeDraftsString({
__path: 'myfile.txt',
size: 0,
size_delta: 0,
}),
''
);
+
+ element.patchRange = parentTo1;
assert.equal(
- element._computeDraftsStringMobile(element.changeComments, parentTo1, {
+ element.computeDraftsStringMobile({
__path: 'myfile.txt',
size: 0,
size_delta: 0,
}),
''
);
+ element.patchRange = _1To2;
assert.equal(
- element._computeDraftsStringMobile(element.changeComments, _1To2, {
+ element.computeDraftsStringMobile({
__path: 'myfile.txt',
size: 0,
size_delta: 0,
}),
''
);
+ element.patchRange = parentTo1;
assert.equal(
- element._computeCommentsStringMobile(
- element.changeComments,
- parentTo1,
- {__path: 'file_added_in_rev2.txt', size: 0, size_delta: 0}
- ),
- ''
- );
- assert.equal(
- element._computeCommentsStringMobile(element.changeComments, _1To2, {
+ element.computeCommentsStringMobile({
__path: 'file_added_in_rev2.txt',
size: 0,
size_delta: 0,
}),
''
);
+ element.patchRange = _1To2;
assert.equal(
- element._computeDraftsString(element.changeComments, parentTo1, {
+ element.computeCommentsStringMobile({
__path: 'file_added_in_rev2.txt',
size: 0,
size_delta: 0,
}),
''
);
+ element.patchRange = parentTo1;
assert.equal(
- element._computeDraftsString(element.changeComments, _1To2, {
+ element.computeDraftsString({
__path: 'file_added_in_rev2.txt',
size: 0,
size_delta: 0,
}),
''
);
+ element.patchRange = _1To2;
assert.equal(
- element._computeDraftsStringMobile(element.changeComments, parentTo1, {
+ element.computeDraftsString({
__path: 'file_added_in_rev2.txt',
size: 0,
size_delta: 0,
}),
''
);
+ element.patchRange = parentTo1;
assert.equal(
- element._computeDraftsStringMobile(element.changeComments, _1To2, {
+ element.computeDraftsStringMobile({
__path: 'file_added_in_rev2.txt',
size: 0,
size_delta: 0,
}),
''
);
+ element.patchRange = _1To2;
assert.equal(
- element._computeCommentsStringMobile(
- element.changeComments,
- parentTo2,
- {__path: '/COMMIT_MSG', size: 0, size_delta: 0}
- ),
+ element.computeDraftsStringMobile({
+ __path: 'file_added_in_rev2.txt',
+ size: 0,
+ size_delta: 0,
+ }),
+ ''
+ );
+ element.patchRange = parentTo2;
+ assert.equal(
+ element.computeCommentsStringMobile({
+ __path: '/COMMIT_MSG',
+ size: 0,
+ size_delta: 0,
+ }),
'1c'
);
+ element.patchRange = _1To2;
assert.equal(
- element._computeCommentsStringMobile(element.changeComments, _1To2, {
+ element.computeCommentsStringMobile({
__path: '/COMMIT_MSG',
size: 0,
size_delta: 0,
}),
'3c'
);
+ element.patchRange = parentTo1;
assert.equal(
- element._computeDraftsString(element.changeComments, parentTo1, {
+ element.computeDraftsString({
__path: '/COMMIT_MSG',
size: 0,
size_delta: 0,
}),
'2 drafts'
);
+ element.patchRange = _1To2;
assert.equal(
- element._computeDraftsString(element.changeComments, _1To2, {
+ element.computeDraftsString({
__path: '/COMMIT_MSG',
size: 0,
size_delta: 0,
}),
'2 drafts'
);
+ element.patchRange = parentTo1;
assert.equal(
- element._computeDraftsStringMobile(element.changeComments, parentTo1, {
+ element.computeDraftsStringMobile({
__path: '/COMMIT_MSG',
size: 0,
size_delta: 0,
}),
'2d'
);
+ element.patchRange = _1To2;
assert.equal(
- element._computeDraftsStringMobile(element.changeComments, _1To2, {
+ element.computeDraftsStringMobile({
__path: '/COMMIT_MSG',
size: 0,
size_delta: 0,
}),
'2d'
);
+ element.patchRange = parentTo2;
assert.equal(
- element._computeCommentsStringMobile(
- element.changeComments,
- parentTo2,
- {__path: 'myfile.txt', size: 0, size_delta: 0}
- ),
+ element.computeCommentsStringMobile({
+ __path: 'myfile.txt',
+ size: 0,
+ size_delta: 0,
+ }),
'2c'
);
+ element.patchRange = _1To2;
assert.equal(
- element._computeCommentsStringMobile(element.changeComments, _1To2, {
+ element.computeCommentsStringMobile({
__path: 'myfile.txt',
size: 0,
size_delta: 0,
}),
'3c'
);
+ element.patchRange = parentTo2;
assert.equal(
- element._computeDraftsStringMobile(element.changeComments, parentTo2, {
+ element.computeDraftsStringMobile({
__path: 'myfile.txt',
size: 0,
size_delta: 0,
}),
''
);
+ element.patchRange = _1To2;
assert.equal(
- element._computeDraftsStringMobile(element.changeComments, _1To2, {
+ element.computeDraftsStringMobile({
__path: 'myfile.txt',
size: 0,
size_delta: 0,
@@ -802,21 +780,21 @@
);
});
- test('_reviewedTitle', () => {
+ test('reviewedTitle', () => {
assert.equal(
- element._reviewedTitle(true),
+ element.reviewedTitle(true),
'Mark as not reviewed (shortcut: r)'
);
assert.equal(
- element._reviewedTitle(false),
+ element.reviewedTitle(false),
'Mark as reviewed (shortcut: r)'
);
});
suite('keyboard shortcuts', () => {
- setup(() => {
- element._filesByPath = {
+ setup(async () => {
+ element.filesByPath = {
'/COMMIT_MSG': {size: 0, size_delta: 0},
'file_added_in_rev2.txt': {size: 0, size_delta: 0},
'myfile.txt': {size: 0, size_delta: 0},
@@ -828,6 +806,8 @@
};
element.change = {_number: 42 as NumericChangeId} as ParsedChangeInfo;
element.fileCursor.setCursorAtIndex(0);
+ await element.updateComplete;
+ await flush();
});
test('toggle left diff via shortcut', () => {
@@ -842,9 +822,7 @@
diffsStub.restore();
});
- test('keyboard shortcuts', () => {
- flush();
-
+ test('keyboard shortcuts', async () => {
const items = [...queryAll<HTMLDivElement>(element, '.file-row')];
element.fileCursor.stops = items;
element.fileCursor.setCursorAtIndex(0);
@@ -904,67 +882,69 @@
assert.isTrue(createCommentInPlaceStub.called);
});
- test('i key shows/hides selected inline diff', () => {
- const paths = Object.keys(element._filesByPath!);
- sinon.stub(element, '_expandedFilesChanged');
- flush();
+ test('i key shows/hides selected inline diff', async () => {
+ const paths = Object.keys(element.filesByPath!);
+ sinon.stub(element, 'expandedFilesChanged');
const files = [...queryAll<HTMLDivElement>(element, '.file-row')];
element.fileCursor.stops = files;
element.fileCursor.setCursorAtIndex(0);
+ await element.updateComplete;
assert.equal(element.diffs.length, 0);
- assert.equal(element._expandedFiles.length, 0);
+ assert.equal(element.expandedFiles.length, 0);
MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
- flush();
+ await element.updateComplete;
assert.equal(element.diffs.length, 1);
assert.equal(element.diffs[0].path, paths[0]);
- assert.equal(element._expandedFiles.length, 1);
- assert.equal(element._expandedFiles[0].path, paths[0]);
+ assert.equal(element.expandedFiles.length, 1);
+ assert.equal(element.expandedFiles[0].path, paths[0]);
MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
- flush();
+ await element.updateComplete;
assert.equal(element.diffs.length, 0);
- assert.equal(element._expandedFiles.length, 0);
+ assert.equal(element.expandedFiles.length, 0);
element.fileCursor.setCursorAtIndex(1);
MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
- flush();
+ await element.updateComplete;
assert.equal(element.diffs.length, 1);
assert.equal(element.diffs[0].path, paths[1]);
- assert.equal(element._expandedFiles.length, 1);
- assert.equal(element._expandedFiles[0].path, paths[1]);
+ assert.equal(element.expandedFiles.length, 1);
+ assert.equal(element.expandedFiles[0].path, paths[1]);
MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'I');
- flush();
+ await element.updateComplete;
assert.equal(element.diffs.length, paths.length);
- assert.equal(element._expandedFiles.length, paths.length);
+ assert.equal(element.expandedFiles.length, paths.length);
for (const diff of element.diffs) {
- assert.isTrue(element._expandedFiles.some(f => f.path === diff.path));
+ assert.isTrue(element.expandedFiles.some(f => f.path === diff.path));
}
// since _expandedFilesChanged is stubbed
element.filesExpanded = FilesExpandedState.ALL;
MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'I');
- flush();
+ await element.updateComplete;
assert.equal(element.diffs.length, 0);
- assert.equal(element._expandedFiles.length, 0);
+ assert.equal(element.expandedFiles.length, 0);
});
- test('r key toggles reviewed flag', () => {
+ test('r key toggles reviewed flag', async () => {
const reducer = (accum: number, file: NormalizedFileInfo) =>
file.isReviewed ? ++accum : accum;
- const getNumReviewed = () => element._files.reduce(reducer, 0);
- flush();
+ const getNumReviewed = () => element.files.reduce(reducer, 0);
+ await element.updateComplete;
// Default state should be unreviewed.
assert.equal(getNumReviewed(), 0);
// Press the review key to toggle it (set the flag).
+ element.handleCursorNext(new KeyboardEvent('keydown'));
MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
- flush();
+ await element.updateComplete;
assert.equal(getNumReviewed(), 1);
// Press the review key to toggle it (clear the flag).
MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ await element.updateComplete;
assert.equal(getNumReviewed(), 0);
});
@@ -972,9 +952,9 @@
let interact: Function;
setup(() => {
- const openCursorStub = sinon.stub(element, '_openCursorFile');
- const openSelectedStub = sinon.stub(element, '_openSelectedFile');
- const expandStub = sinon.stub(element, '_toggleFileExpanded');
+ const openCursorStub = sinon.stub(element, 'openCursorFile');
+ const openSelectedStub = sinon.stub(element, 'openSelectedFile');
+ const expandStub = sinon.stub(element, 'toggleFileExpanded');
interact = function () {
openCursorStub.reset();
@@ -1016,9 +996,7 @@
const moveRightStub = sinon.stub(element.diffCursor, 'moveRight');
let noDiffsExpanded = true;
- sinon
- .stub(element, '_noDiffsExpanded')
- .callsFake(() => noDiffsExpanded);
+ sinon.stub(element, 'noDiffsExpanded').callsFake(() => noDiffsExpanded);
MockInteractions.pressAndReleaseKeyOn(
element,
@@ -1054,25 +1032,26 @@
});
});
- test('file review status', () => {
+ test('file review status', async () => {
element.reviewed = ['/COMMIT_MSG', 'myfile.txt'];
- element._filesByPath = {
+ element.filesByPath = {
'/COMMIT_MSG': {size: 0, size_delta: 0},
'file_added_in_rev2.txt': {size: 0, size_delta: 0},
'myfile.txt': {size: 0, size_delta: 0},
};
- element._loggedIn = true;
+ element.loggedIn = true;
element.changeNum = 42 as NumericChangeId;
element.patchRange = {
basePatchNum: 'PARENT' as BasePatchSetNum,
patchNum: 2 as RevisionPatchSetNum,
};
element.fileCursor.setCursorAtIndex(0);
- const reviewSpy = sinon.spy(element, '_reviewFile');
- const toggleExpandSpy = sinon.spy(element, '_toggleFileExpanded');
- flush();
- queryAll(element, '.row:not(.header-row)');
+ const reviewSpy = sinon.spy(element, 'reviewFile');
+ const toggleExpandSpy = sinon.spy(element, 'toggleFileExpanded');
+
+ await element.updateComplete;
+
const fileRows = queryAll(element, '.row:not(.header-row)');
const checkSelector = 'span.reviewedSwitch[role="switch"]';
const commitMsg = fileRows[0].querySelector(checkSelector);
@@ -1084,19 +1063,26 @@
assert.equal(myFile!.getAttribute('aria-checked'), 'true');
const commitReviewLabel = fileRows[0].querySelector('.reviewedLabel');
+ assert.isOk(commitReviewLabel);
const markReviewLabel = fileRows[0].querySelector('.markReviewed');
+ assert.isOk(markReviewLabel);
assert.isTrue(commitReviewLabel!.classList.contains('isReviewed'));
assert.equal(markReviewLabel!.textContent, 'MARK UNREVIEWED');
- const clickSpy = sinon.spy(element, '_reviewedClick');
+ const clickSpy = sinon.spy(element, 'reviewedClick');
MockInteractions.tap(markReviewLabel!);
+ await element.updateComplete;
+
// assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', false));
// assert.isFalse(commitReviewLabel.classList.contains('isReviewed'));
assert.equal(markReviewLabel!.textContent, 'MARK REVIEWED');
+ assert.isTrue(clickSpy.calledOnce);
assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented);
assert.isTrue(reviewSpy.calledOnce);
MockInteractions.tap(markReviewLabel!);
+ await element.updateComplete;
+
assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', true));
assert.isTrue(commitReviewLabel!.classList.contains('isReviewed'));
assert.equal(markReviewLabel!.textContent, 'MARK UNREVIEWED');
@@ -1106,8 +1092,8 @@
assert.isFalse(toggleExpandSpy.called);
});
- test('_handleFileListClick', () => {
- element._filesByPath = {
+ test('handleFileListClick', async () => {
+ element.filesByPath = {
'/COMMIT_MSG': {size: 0, size_delta: 0},
'f1.txt': {size: 0, size_delta: 0},
'f2.txt': {size: 0, size_delta: 0},
@@ -1117,33 +1103,36 @@
basePatchNum: 'PARENT' as BasePatchSetNum,
patchNum: 2 as RevisionPatchSetNum,
};
+ await element.updateComplete;
- const clickSpy = sinon.spy(element, '_handleFileListClick');
- const reviewStub = sinon.stub(element, '_reviewFile');
- const toggleExpandSpy = sinon.spy(element, '_toggleFileExpanded');
+ const clickSpy = sinon.spy(element, 'handleFileListClick');
+ const reviewStub = sinon.stub(element, 'reviewFile');
+ const toggleExpandSpy = sinon.spy(element, 'toggleFileExpanded');
const row = queryAndAssert(
element,
'.row[data-file=\'{"path":"f1.txt"}\']'
);
- // Click on the expand button, resulting in _toggleFileExpanded being
- // called and not resulting in a call to _reviewFile.
+ // Click on the expand button, resulting in toggleFileExpanded being
+ // called and not resulting in a call to reviewFile.
queryAndAssert<HTMLDivElement>(row, 'div.show-hide').click();
+ await element.updateComplete;
assert.isTrue(clickSpy.calledOnce);
assert.isTrue(toggleExpandSpy.calledOnce);
assert.isFalse(reviewStub.called);
// Click inside the diff. This should result in no additional calls to
- // _toggleFileExpanded or _reviewFile.
+ // toggleFileExpanded or reviewFile.
queryAndAssert<GrDiffHost>(element, 'gr-diff-host').click();
+ await element.updateComplete;
assert.isTrue(clickSpy.calledTwice);
assert.isTrue(toggleExpandSpy.calledOnce);
assert.isFalse(reviewStub.called);
});
- test('_handleFileListClick editMode', () => {
- element._filesByPath = {
+ test('handleFileListClick editMode', async () => {
+ element.filesByPath = {
'/COMMIT_MSG': {size: 0, size_delta: 0},
'f1.txt': {size: 0, size_delta: 0},
'f2.txt': {size: 0, size_delta: 0},
@@ -1154,18 +1143,21 @@
patchNum: 2 as RevisionPatchSetNum,
};
element.editMode = true;
- flush();
- const clickSpy = sinon.spy(element, '_handleFileListClick');
- const toggleExpandSpy = sinon.spy(element, '_toggleFileExpanded');
+ await element.updateComplete;
- // Tap the edit controls. Should be ignored by _handleFileListClick.
+ const clickSpy = sinon.spy(element, 'handleFileListClick');
+ const toggleExpandSpy = sinon.spy(element, 'toggleFileExpanded');
+
+ // Tap the edit controls. Should be ignored by handleFileListClick.
MockInteractions.tap(queryAndAssert(element, '.editFileControls'));
+ await element.updateComplete;
+
assert.isTrue(clickSpy.calledOnce);
assert.isFalse(toggleExpandSpy.called);
});
- test('checkbox shows/hides diff inline', () => {
- element._filesByPath = {
+ test('checkbox shows/hides diff inline', async () => {
+ element.filesByPath = {
'myfile.txt': {size: 0, size_delta: 0},
};
element.changeNum = 42 as NumericChangeId;
@@ -1174,8 +1166,8 @@
patchNum: 2 as RevisionPatchSetNum,
};
element.fileCursor.setCursorAtIndex(0);
- sinon.stub(element, '_expandedFilesChanged');
- flush();
+ sinon.stub(element, 'expandedFilesChanged');
+ await element.updateComplete;
const fileRows = queryAll(element, '.row:not(.header-row)');
// Because the label surrounds the input, the tap event is triggered
// there first.
@@ -1185,15 +1177,17 @@
const showHideLabel = showHideCheck!.querySelector('.show-hide-icon');
assert.equal(showHideCheck!.getAttribute('aria-checked'), 'false');
MockInteractions.tap(showHideLabel!);
+ await element.updateComplete;
+
assert.equal(showHideCheck!.getAttribute('aria-checked'), 'true');
assert.notEqual(
- element._expandedFiles.findIndex(f => f.path === 'myfile.txt'),
+ element.expandedFiles.findIndex(f => f.path === 'myfile.txt'),
-1
);
});
- test('diff mode correctly toggles the diffs', () => {
- element._filesByPath = {
+ test('diff mode correctly toggles the diffs', async () => {
+ element.filesByPath = {
'myfile.txt': {size: 0, size_delta: 0},
};
element.changeNum = 42 as NumericChangeId;
@@ -1201,28 +1195,30 @@
basePatchNum: 'PARENT' as BasePatchSetNum,
patchNum: 2 as RevisionPatchSetNum,
};
- const updateDiffPrefSpy = sinon.spy(element, '_updateDiffPreferences');
+ const updateDiffPrefSpy = sinon.spy(element, 'updateDiffPreferences');
element.fileCursor.setCursorAtIndex(0);
- flush();
+ await element.updateComplete;
// Tap on a file to generate the diff.
const row = queryAll(element, '.row:not(.header-row) span.show-hide')[0];
MockInteractions.tap(row);
- flush();
- element.set('diffViewMode', 'UNIFIED_DIFF');
+
+ element.diffViewMode = DiffViewMode.UNIFIED;
+ await element.updateComplete;
+
assert.isTrue(updateDiffPrefSpy.called);
});
test('expanded attribute not set on path when not expanded', () => {
- element._filesByPath = {
+ element.filesByPath = {
'/COMMIT_MSG': {size: 0, size_delta: 0},
};
assert.isNotOk(query(element, 'expanded'));
});
- test('tapping row ignores links', () => {
- element._filesByPath = {
+ test('tapping row ignores links', async () => {
+ element.filesByPath = {
'/COMMIT_MSG': {size: 0, size_delta: 0},
};
element.changeNum = 42 as NumericChangeId;
@@ -1230,8 +1226,8 @@
basePatchNum: 'PARENT' as BasePatchSetNum,
patchNum: 2 as RevisionPatchSetNum,
};
- sinon.stub(element, '_expandedFilesChanged');
- flush();
+ sinon.stub(element, 'expandedFilesChanged');
+ await element.updateComplete;
const commitMsgFile = queryAll(
element,
'.row:not(.header-row) a.pathLink'
@@ -1239,10 +1235,10 @@
// Remove href attribute so the app doesn't route to a diff view
commitMsgFile.removeAttribute('href');
- const togglePathSpy = sinon.spy(element, '_toggleFileExpanded');
+ const togglePathSpy = sinon.spy(element, 'toggleFileExpanded');
MockInteractions.tap(commitMsgFile);
- flush();
+ await element.updateComplete;
assert(togglePathSpy.notCalled, 'file is opened as diff view');
assert.isNotOk(query(element, '.expanded'));
assert.notEqual(
@@ -1251,19 +1247,26 @@
);
});
- test('_toggleFileExpanded', () => {
+ test('toggleFileExpanded', async () => {
const path = 'path/to/my/file.txt';
- element._filesByPath = {[path]: {size: 0, size_delta: 0}};
- const renderSpy = sinon.spy(element, '_renderInOrder');
- const collapseStub = sinon.stub(element, '_clearCollapsedDiffs');
+ element.filesByPath = {[path]: {size: 0, size_delta: 0}};
+ await element.updateComplete;
+ // Wait for expandedFilesChanged to finish.
+ await flush();
+
+ const renderSpy = sinon.spy(element, 'renderInOrder');
+ const collapseStub = sinon.stub(element, 'clearCollapsedDiffs');
assert.equal(
queryAndAssert<IronIconElement>(element, 'iron-icon').icon,
'gr-icons:expand-more'
);
- assert.equal(element._expandedFiles.length, 0);
- element._toggleFileExpanded({path});
- flush();
+ assert.equal(element.expandedFiles.length, 0);
+ element.toggleFileExpanded({path});
+ await element.updateComplete;
+ // Wait for expandedFilesChanged to finish.
+ await flush();
+
assert.equal(collapseStub.lastCall.args[0].length, 0);
assert.equal(
queryAndAssert<IronIconElement>(element, 'iron-icon').icon,
@@ -1271,45 +1274,49 @@
);
assert.equal(renderSpy.callCount, 1);
- assert.isTrue(element._expandedFiles.some(f => f.path === path));
- element._toggleFileExpanded({path});
- flush();
+ assert.isTrue(element.expandedFiles.some(f => f.path === path));
+ element.toggleFileExpanded({path});
+ await element.updateComplete;
+ // Wait for expandedFilesChanged to finish.
+ await flush();
assert.equal(
queryAndAssert<IronIconElement>(element, 'iron-icon').icon,
'gr-icons:expand-more'
);
assert.equal(renderSpy.callCount, 1);
- assert.isFalse(element._expandedFiles.some(f => f.path === path));
+ assert.isFalse(element.expandedFiles.some(f => f.path === path));
assert.equal(collapseStub.lastCall.args[0].length, 1);
});
- test('expandAllDiffs and collapseAllDiffs', () => {
- const collapseStub = sinon.stub(element, '_clearCollapsedDiffs');
- const cursorUpdateStub = sinon.stub(
- element.diffCursor,
- 'handleDiffUpdate'
- );
+ test('expandAllDiffs and collapseAllDiffs', async () => {
+ const collapseStub = sinon.stub(element, 'clearCollapsedDiffs');
const reInitStub = sinon.stub(element.diffCursor, 'reInitAndUpdateStops');
const path = 'path/to/my/file.txt';
- element._filesByPath = {[path]: {size: 0, size_delta: 0}};
+ element.filesByPath = {[path]: {size: 0, size_delta: 0}};
+ // Wait for diffs to be computed.
+ await element.updateComplete;
+ await flush();
element.expandAllDiffs();
- flush();
+ await element.updateComplete;
+ // Wait for expandedFilesChanged to finish.
+ await flush();
assert.equal(element.filesExpanded, FilesExpandedState.ALL);
- assert.isTrue(reInitStub.calledOnce);
+ assert.isTrue(reInitStub.calledTwice);
assert.equal(collapseStub.lastCall.args[0].length, 0);
element.collapseAllDiffs();
- flush();
- assert.equal(element._expandedFiles.length, 0);
+ await element.updateComplete;
+ // Wait for expandedFilesChanged to finish.
+ await flush();
+ assert.equal(element.expandedFiles.length, 0);
assert.equal(element.filesExpanded, FilesExpandedState.NONE);
- assert.isTrue(cursorUpdateStub.calledOnce);
assert.equal(collapseStub.lastCall.args[0].length, 1);
});
- test('_expandedFilesChanged', async () => {
- sinon.stub(element, '_reviewFile');
+ test('expandedFilesChanged', async () => {
+ sinon.stub(element, 'reviewFile');
const path = 'path/to/my/file.txt';
const promise = mockPromise();
const diffs = [
@@ -1335,11 +1342,13 @@
},
];
sinon.stub(element, 'diffs').get(() => diffs);
- element.push('_expandedFiles', {path});
+ element.expandedFiles = element.expandedFiles.concat([{path}]);
+ await element.updateComplete;
+ await flush();
await promise;
});
- test('_clearCollapsedDiffs', () => {
+ test('clearCollapsedDiffs', () => {
// Have to type as any because the type is 'GrDiffHost'
// which would require stubbing so many different
// methods / properties that it isn't worth it.
@@ -1347,34 +1356,36 @@
cancel: sinon.stub(),
clearDiffContent: sinon.stub(),
} as any;
- element._clearCollapsedDiffs([diff]);
+ element.clearCollapsedDiffs([diff]);
assert.isTrue(diff.cancel.calledOnce);
assert.isTrue(diff.clearDiffContent.calledOnce);
});
- test('filesExpanded value updates to correct enum', () => {
- element._filesByPath = {
+ test('filesExpanded value updates to correct enum', async () => {
+ element.filesByPath = {
'foo.bar': {size: 0, size_delta: 0},
'baz.bar': {size: 0, size_delta: 0},
};
- flush();
+ await element.updateComplete;
assert.equal(element.filesExpanded, FilesExpandedState.NONE);
- element.push('_expandedFiles', {path: 'baz.bar'});
- flush();
+ element.expandedFiles.push({path: 'baz.bar'});
+ element.expandedFilesChanged([{path: 'baz.bar'}]);
+ await element.updateComplete;
assert.equal(element.filesExpanded, FilesExpandedState.SOME);
- element.push('_expandedFiles', {path: 'foo.bar'});
- flush();
+ element.expandedFiles.push({path: 'foo.bar'});
+ element.expandedFilesChanged([{path: 'foo.bar'}]);
+ await element.updateComplete;
assert.equal(element.filesExpanded, FilesExpandedState.ALL);
element.collapseAllDiffs();
- flush();
+ await element.updateComplete;
assert.equal(element.filesExpanded, FilesExpandedState.NONE);
element.expandAllDiffs();
- flush();
+ await element.updateComplete;
assert.equal(element.filesExpanded, FilesExpandedState.ALL);
});
- test('_renderInOrder', async () => {
- const reviewStub = sinon.stub(element, '_reviewFile');
+ test('renderInOrder', async () => {
+ const reviewStub = sinon.stub(element, 'reviewFile');
let callCount = 0;
// Have to type as any because the type is 'GrDiffHost'
// which would require stubbing so many different
@@ -1408,18 +1419,14 @@
},
},
] as any;
- element._renderInOrder(
- [{path: 'p2'}, {path: 'p1'}, {path: 'p0'}],
- diffs,
- 3
- );
- await flush();
+ element.renderInOrder([{path: 'p2'}, {path: 'p1'}, {path: 'p0'}], diffs);
+ await element.updateComplete;
assert.isFalse(reviewStub.called);
});
- test('_renderInOrder logged in', async () => {
- element._loggedIn = true;
- const reviewStub = sinon.stub(element, '_reviewFile');
+ test('renderInOrder logged in', async () => {
+ element.loggedIn = true;
+ const reviewStub = sinon.stub(element, 'reviewFile');
let callCount = 0;
// Have to type as any because the type is 'GrDiffHost'
// which would require stubbing so many different
@@ -1436,15 +1443,28 @@
},
},
] as any;
- element._renderInOrder([{path: 'p2'}], diffs, 1);
- await flush();
+ element.renderInOrder([{path: 'p2'}], diffs);
+ await element.updateComplete;
assert.equal(reviewStub.callCount, 1);
});
- test('_renderInOrder respects diffPrefs.manual_review', async () => {
- element._loggedIn = true;
- element.diffPrefs = {manual_review: true} as DiffPreferencesInfo;
- const reviewStub = sinon.stub(element, '_reviewFile');
+ test('renderInOrder respects diffPrefs.manual_review', async () => {
+ element.loggedIn = true;
+ element.diffPrefs = {
+ context: 10,
+ tab_size: 8,
+ font_size: 12,
+ line_length: 100,
+ cursor_blink_rate: 0,
+ line_wrapping: false,
+ show_line_endings: true,
+ show_tabs: true,
+ show_whitespace_errors: true,
+ syntax_highlighting: true,
+ ignore_whitespace: 'IGNORE_NONE',
+ manual_review: true,
+ };
+ const reviewStub = sinon.stub(element, 'reviewFile');
// Have to type as any because the type is 'GrDiffHost'
// which would require stubbing so many different
// methods / properties that it isn't worth it.
@@ -1459,17 +1479,19 @@
},
] as any;
- element._renderInOrder([{path: 'p'}], diffs, 1);
- await flush();
+ element.renderInOrder([{path: 'p'}], diffs);
+ await element.updateComplete;
assert.isFalse(reviewStub.called);
delete element.diffPrefs.manual_review;
- element._renderInOrder([{path: 'p'}], diffs, 1);
+ element.renderInOrder([{path: 'p'}], diffs);
+ await element.updateComplete;
+ // Wait for renderInOrder to finish
await flush();
assert.isTrue(reviewStub.called);
assert.isTrue(reviewStub.calledWithExactly('p', true));
});
- test('_loadingChanged fired from reload in debouncer', async () => {
+ test('loadingChanged fired from reload in debouncer', async () => {
const reloadBlocker = mockPromise();
stubRestApi('getChangeOrEditFiles').resolves({
'foo.bar': {size: 0, size_delta: 0},
@@ -1480,14 +1502,17 @@
element.changeNum = 123 as NumericChangeId;
element.patchRange = {patchNum: 12 as RevisionPatchSetNum} as PatchRange;
- element._filesByPath = {'foo.bar': {size: 0, size_delta: 0}};
+ element.filesByPath = {'foo.bar': {size: 0, size_delta: 0}};
element.change = {
...createParsedChange(),
_number: 123 as NumericChangeId,
};
+ await element.updateComplete;
+ await flush();
const reloaded = element.reload();
- assert.isTrue(element._loading);
+ await element.updateComplete;
+ assert.isTrue(element.loading);
assert.isFalse(element.classList.contains('loading'));
element.loadingTask!.flush();
assert.isTrue(element.classList.contains('loading'));
@@ -1495,15 +1520,14 @@
reloadBlocker.resolve();
await reloaded;
- assert.isFalse(element._loading);
+ assert.isFalse(element.loading);
element.loadingTask!.flush();
assert.isFalse(element.classList.contains('loading'));
});
- test('_loadingChanged does not set class when there are no files', () => {
+ test('loadingChanged does not set class when there are no files', () => {
const reloadBlocker = mockPromise();
stubRestApi('getLoggedIn').returns(reloadBlocker.then(() => false));
- sinon.stub(element, '_getReviewedFiles').resolves([]);
element.changeNum = 123 as NumericChangeId;
element.patchRange = {patchNum: 12 as RevisionPatchSetNum} as PatchRange;
element.change = {
@@ -1512,7 +1536,7 @@
};
element.reload();
- assert.isTrue(element._loading);
+ assert.isTrue(element.loading);
element.loadingTask!.flush();
@@ -1554,12 +1578,13 @@
basePatchNum: 'PARENT' as BasePatchSetNum,
patchNum: 1 as RevisionPatchSetNum,
};
+ await element.updateComplete;
await flush();
});
test('displays cleanly merged file count', async () => {
await element.reload();
- await flush();
+ await element.updateComplete;
const message = queryAndAssert<HTMLSpanElement>(
element,
@@ -1580,7 +1605,7 @@
'anotherCleanlyMergedFile.js': {size: 0, size_delta: 0},
});
await element.reload();
- await flush();
+ await element.updateComplete;
const message = queryAndAssert(
element,
@@ -1591,7 +1616,7 @@
test('displays button for navigating to parent 1 base', async () => {
await element.reload();
- await flush();
+ await element.updateComplete;
queryAndAssert(element, '.showParentButton');
});
@@ -1611,9 +1636,9 @@
},
});
await element.reload();
- await flush();
+ await element.updateComplete;
- assert.deepEqual(element._cleanlyMergedOldPaths, [
+ assert.deepEqual(element.cleanlyMergedOldPaths, [
'cleanlyMergedFileOldName.js',
]);
});
@@ -1624,7 +1649,7 @@
patchNum: 2 as RevisionPatchSetNum,
};
await element.reload();
- await flush();
+ await element.updateComplete;
assert.notOk(query(element, '.cleanlyMergedText'));
assert.notOk(query(element, '.showParentButton'));
@@ -1636,7 +1661,7 @@
patchNum: EditPatchSetNum,
};
await element.reload();
- await flush();
+ await element.updateComplete;
assert.notOk(query(element, '.cleanlyMergedText'));
assert.notOk(query(element, '.showParentButton'));
@@ -1649,22 +1674,18 @@
const diffStub = sinon
.stub(GerritNav, 'getUrlForDiff')
.returns('/c/gerrit/+/1/1/index.php');
- const change = {
+ element.change = {
...createParsedChange(),
_number: 1 as NumericChangeId,
project: 'gerrit' as RepoName,
};
+ element.patchRange = {
+ basePatchNum: ParentPatchSetNum,
+ patchNum: 1 as RevisionPatchSetNum,
+ };
const path = 'index.php';
- assert.equal(
- element._computeDiffURL(
- change,
- undefined,
- 1 as RevisionPatchSetNum,
- path,
- false
- ),
- '/c/gerrit/+/1/1/index.php'
- );
+ element.editMode = false;
+ assert.equal(element.computeDiffURL(path), '/c/gerrit/+/1/1/index.php');
diffStub.restore();
});
@@ -1672,22 +1693,18 @@
const diffStub = sinon
.stub(GerritNav, 'getUrlForDiff')
.returns('/c/gerrit/+/1/1//COMMIT_MSG');
- const change = {
+ element.change = {
...createParsedChange(),
_number: 1 as NumericChangeId,
project: 'gerrit' as RepoName,
};
+ element.patchRange = {
+ basePatchNum: ParentPatchSetNum,
+ patchNum: 1 as RevisionPatchSetNum,
+ };
+ element.editMode = false;
const path = '/COMMIT_MSG';
- assert.equal(
- element._computeDiffURL(
- change,
- undefined,
- 1 as RevisionPatchSetNum,
- path,
- false
- ),
- '/c/gerrit/+/1/1//COMMIT_MSG'
- );
+ assert.equal(element.computeDiffURL(path), '/c/gerrit/+/1/1//COMMIT_MSG');
diffStub.restore();
});
@@ -1695,20 +1712,19 @@
const editStub = sinon
.stub(GerritNav, 'getEditUrlForDiff')
.returns('/c/gerrit/+/1/edit/index.php,edit');
- const change = {
+ element.change = {
...createParsedChange(),
_number: 1 as NumericChangeId,
project: 'gerrit' as RepoName,
};
+ element.patchRange = {
+ basePatchNum: ParentPatchSetNum,
+ patchNum: 1 as RevisionPatchSetNum,
+ };
+ element.editMode = true;
const path = 'index.php';
assert.equal(
- element._computeDiffURL(
- change,
- undefined,
- 1 as RevisionPatchSetNum,
- path,
- true
- ),
+ element.computeDiffURL(path),
'/c/gerrit/+/1/edit/index.php,edit'
);
editStub.restore();
@@ -1718,20 +1734,19 @@
const editStub = sinon
.stub(GerritNav, 'getEditUrlForDiff')
.returns('/c/gerrit/+/1/edit//COMMIT_MSG,edit');
- const change = {
+ element.change = {
...createParsedChange(),
_number: 1 as NumericChangeId,
project: 'gerrit' as RepoName,
};
+ element.patchRange = {
+ basePatchNum: ParentPatchSetNum,
+ patchNum: 1 as RevisionPatchSetNum,
+ };
+ element.editMode = true;
const path = '/COMMIT_MSG';
assert.equal(
- element._computeDiffURL(
- change,
- undefined,
- 1 as RevisionPatchSetNum,
- path,
- true
- ),
+ element.computeDiffURL(path),
'/c/gerrit/+/1/edit//COMMIT_MSG,edit'
);
editStub.restore();
@@ -1739,7 +1754,7 @@
});
suite('size bars', () => {
- test('_computeSizeBarLayout', () => {
+ test('computeSizeBarLayout', async () => {
const defaultSizeBarLayout = {
maxInserted: 0,
maxDeleted: 0,
@@ -1748,37 +1763,39 @@
deletionOffset: 0,
};
- assert.deepEqual(
- element._computeSizeBarLayout(undefined),
- defaultSizeBarLayout
- );
- assert.deepEqual(
- element._computeSizeBarLayout(
- {} as PolymerDeepPropertyChange<
- NormalizedFileInfo[],
- NormalizedFileInfo[]
- >
- ),
- defaultSizeBarLayout
- );
- assert.deepEqual(
- element._computeSizeBarLayout({base: []} as any),
- defaultSizeBarLayout
- );
+ element.files = [];
+ await element.updateComplete;
+ assert.deepEqual(element.computeSizeBarLayout(), defaultSizeBarLayout);
- const files = [
- {__path: '/COMMIT_MSG', lines_inserted: 10000},
- {__path: 'foo', lines_inserted: 4, lines_deleted: 10},
- {__path: 'bar', lines_inserted: 5, lines_deleted: 8},
+ element.files = [
+ {
+ __path: '/COMMIT_MSG',
+ lines_inserted: 10000,
+ size_delta: 10000,
+ size: 10000,
+ },
+ {
+ __path: 'foo',
+ lines_inserted: 4,
+ lines_deleted: 10,
+ size_delta: 14,
+ size: 20,
+ },
+ {
+ __path: 'bar',
+ lines_inserted: 5,
+ lines_deleted: 8,
+ size_delta: 13,
+ size: 21,
+ },
];
- const layout = element._computeSizeBarLayout({
- base: files,
- } as PolymerDeepPropertyChange<NormalizedFileInfo[], NormalizedFileInfo[]>);
+ await element.updateComplete;
+ const layout = element.computeSizeBarLayout();
assert.equal(layout.maxInserted, 5);
assert.equal(layout.maxDeleted, 10);
});
- test('_computeBarAdditionWidth', () => {
+ test('computeBarAdditionWidth', () => {
const file = {
__path: 'foo/bar.baz',
lines_inserted: 5,
@@ -1796,27 +1813,27 @@
// Uses half the space when file is half the largest addition and there
// are no deletions.
- assert.equal(element._computeBarAdditionWidth(file, stats), 30);
+ assert.equal(element.computeBarAdditionWidth(file, stats), 30);
// If there are no insertions, there is no width.
stats.maxInserted = 0;
- assert.equal(element._computeBarAdditionWidth(file, stats), 0);
+ assert.equal(element.computeBarAdditionWidth(file, stats), 0);
// If the insertions is not present on the file, there is no width.
stats.maxInserted = 10;
file.lines_inserted = 0;
- assert.equal(element._computeBarAdditionWidth(file, stats), 0);
+ assert.equal(element.computeBarAdditionWidth(file, stats), 0);
// If the file is a commit message, returns zero.
file.lines_inserted = 5;
file.__path = '/COMMIT_MSG';
- assert.equal(element._computeBarAdditionWidth(file, stats), 0);
+ assert.equal(element.computeBarAdditionWidth(file, stats), 0);
// Width bottoms-out at the minimum width.
file.__path = 'stuff.txt';
file.lines_inserted = 1;
stats.maxInserted = 1000000;
- assert.equal(element._computeBarAdditionWidth(file, stats), 1.5);
+ assert.equal(element.computeBarAdditionWidth(file, stats), 1.5);
});
test('_computeBarAdditionX', () => {
@@ -1834,10 +1851,10 @@
maxDeletionWidth: 0,
deletionOffset: 60,
};
- assert.equal(element._computeBarAdditionX(file, stats), 30);
+ assert.equal(element.computeBarAdditionX(file, stats), 30);
});
- test('_computeBarDeletionWidth', () => {
+ test('computeBarDeletionWidth', () => {
const file = {
__path: 'foo/bar.baz',
lines_inserted: 0,
@@ -1855,42 +1872,41 @@
// Uses a quarter the space when file is half the largest deletions and
// there are equal additions.
- assert.equal(element._computeBarDeletionWidth(file, stats), 15);
+ assert.equal(element.computeBarDeletionWidth(file, stats), 15);
// If there are no deletions, there is no width.
stats.maxDeleted = 0;
- assert.equal(element._computeBarDeletionWidth(file, stats), 0);
+ assert.equal(element.computeBarDeletionWidth(file, stats), 0);
// If the deletions is not present on the file, there is no width.
stats.maxDeleted = 10;
file.lines_deleted = 0;
- assert.equal(element._computeBarDeletionWidth(file, stats), 0);
+ assert.equal(element.computeBarDeletionWidth(file, stats), 0);
// If the file is a commit message, returns zero.
file.lines_deleted = 5;
file.__path = '/COMMIT_MSG';
- assert.equal(element._computeBarDeletionWidth(file, stats), 0);
+ assert.equal(element.computeBarDeletionWidth(file, stats), 0);
// Width bottoms-out at the minimum width.
file.__path = 'stuff.txt';
file.lines_deleted = 1;
stats.maxDeleted = 1000000;
- assert.equal(element._computeBarDeletionWidth(file, stats), 1.5);
+ assert.equal(element.computeBarDeletionWidth(file, stats), 1.5);
});
test('_computeSizeBarsClass', () => {
+ element.showSizeBars = false;
assert.equal(
- element._computeSizeBarsClass(false, 'foo/bar.baz'),
+ element.computeSizeBarsClass('foo/bar.baz'),
'sizeBars hide'
);
+ element.showSizeBars = true;
assert.equal(
- element._computeSizeBarsClass(true, '/COMMIT_MSG'),
+ element.computeSizeBarsClass('/COMMIT_MSG'),
'sizeBars invisible'
);
- assert.equal(
- element._computeSizeBarsClass(true, 'foo/bar.baz'),
- 'sizeBars '
- );
+ assert.equal(element.computeSizeBarsClass('foo/bar.baz'), 'sizeBars ');
});
});
@@ -1958,7 +1974,7 @@
await setupDiff(diffs[i]);
}
- element._updateDiffCursor();
+ element.updateDiffCursor();
element.diffCursor.handleDiffUpdate();
return diffs;
}
@@ -1974,22 +1990,31 @@
stub('gr-diff-host', 'reload').callsFake(() => Promise.resolve());
stub('gr-diff-host', 'prefetchDiff').callsFake(() => {});
- // Element must be wrapped in an element with direct access to the
- // comment API.
- commentApiWrapper = basicFixture.instantiate();
- element = commentApiWrapper.$.fileList;
- element.diffPrefs = {} as DiffPreferencesInfo;
+ element = basicFixture.instantiate();
+ element.diffPrefs = {
+ context: 10,
+ tab_size: 8,
+ font_size: 12,
+ line_length: 100,
+ cursor_blink_rate: 0,
+ line_wrapping: false,
+ show_line_endings: true,
+ show_tabs: true,
+ show_whitespace_errors: true,
+ syntax_highlighting: true,
+ ignore_whitespace: 'IGNORE_NONE',
+ };
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
project: 'testRepo' as RepoName,
};
- reviewFileStub = sinon.stub(element, '_reviewFile');
+ reviewFileStub = sinon.stub(element, 'reviewFile');
- element._loading = false;
+ element.loading = false;
element.numFilesShown = 75;
element.selectedIndex = 0;
- element._filesByPath = {
+ element.filesByPath = {
'/COMMIT_MSG': {lines_inserted: 9, size: 0, size_delta: 0},
'file_added_in_rev2.txt': {
lines_inserted: 1,
@@ -2005,7 +2030,7 @@
},
};
element.reviewed = ['/COMMIT_MSG', 'myfile.txt'];
- element._loggedIn = true;
+ element.loggedIn = true;
element.changeNum = 42 as NumericChangeId;
element.patchRange = {
basePatchNum: 'PARENT' as BasePatchSetNum,
@@ -2014,12 +2039,13 @@
sinon
.stub(window, 'fetch')
.callsFake(() => Promise.resolve(new Response()));
- await flush();
+ await element.updateComplete;
});
test('cursor with individually opened files', async () => {
MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
- await flush();
+ await element.updateComplete;
+
let diffs = await renderAndGetNewDiffs(0);
const diffStops = diffs[0].getCursorStops();
@@ -2035,7 +2061,7 @@
MockInteractions.tap(
queryAll(diffStops[10] as HTMLElement, '.contentText')[0]
);
- await flush();
+ await element.updateComplete;
assert.isTrue(
(diffStops[10] as HTMLElement).classList.contains('target-row')
);
@@ -2043,7 +2069,7 @@
// Keyboard shortcuts are still moving the file cursor, not the diff
// cursor.
MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
- await flush();
+ await element.updateComplete;
assert.isTrue(
(diffStops[10] as HTMLElement).classList.contains('target-row')
);
@@ -2055,7 +2081,7 @@
assert.equal(element.fileCursor.index, 1);
MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
- await flush();
+ await element.updateComplete;
diffs = await renderAndGetNewDiffs(1);
// Two diffs should be rendered.
@@ -2074,7 +2100,7 @@
test('cursor with toggle all files', async () => {
MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'I');
- await flush();
+ await element.updateComplete;
const diffs = await renderAndGetNewDiffs(0);
const diffStops = diffs[0].getCursorStops();
@@ -2091,7 +2117,7 @@
MockInteractions.tap(
queryAll(diffStops[10] as HTMLElement, '.contentText')[0]
);
- await flush();
+ await element.updateComplete;
assert.isTrue(
(diffStops[10] as HTMLElement).classList.contains('target-row')
);
@@ -2099,7 +2125,7 @@
// Keyboard shortcuts are still moving the file cursor, not the diff
// cursor.
MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
- await flush();
+ await element.updateComplete;
assert.isFalse(
(diffStops[10] as HTMLElement).classList.contains('target-row')
);
@@ -2117,7 +2143,7 @@
let fileRows: NodeListOf<HTMLDivElement>;
setup(() => {
- sinon.stub(element, '_renderInOrder').returns(Promise.resolve());
+ sinon.stub(element, 'renderInOrder').returns(Promise.resolve());
nextCommentStub = sinon.stub(
element.diffCursor,
'moveToNextCommentThread'
@@ -2126,72 +2152,77 @@
fileRows = queryAll<HTMLDivElement>(element, '.row:not(.header-row)');
});
- test('n key with some files expanded', async () => {
+ test('correct number of files expanded', async () => {
MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, null, 'i');
- await flush();
+ await element.updateComplete;
assert.equal(element.filesExpanded, FilesExpandedState.SOME);
MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
+ await element.updateComplete;
assert.isTrue(nextChunkStub.calledOnce);
});
test('N key with some files expanded', async () => {
MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, null, 'i');
- await flush();
+ await element.updateComplete;
assert.equal(element.filesExpanded, FilesExpandedState.SOME);
MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'N');
+ await element.updateComplete;
assert.isTrue(nextCommentStub.calledOnce);
});
test('n key with all files expanded', async () => {
MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, null, 'I');
- await flush();
+ await element.updateComplete;
assert.equal(element.filesExpanded, FilesExpandedState.ALL);
MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
+ await element.updateComplete;
assert.isTrue(nextChunkStub.calledOnce);
});
test('N key with all files expanded', async () => {
MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, null, 'I');
- await flush();
+ await element.updateComplete;
assert.equal(element.filesExpanded, FilesExpandedState.ALL);
MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'N');
+ await element.updateComplete;
assert.isTrue(nextCommentStub.called);
});
});
- test('_openSelectedFile behavior', async () => {
- const _filesByPath = element._filesByPath;
- element.set('_filesByPath', {});
+ test('openSelectedFile behavior', async () => {
+ const filesByPath = element.filesByPath;
+ element.filesByPath = {};
+ await element.updateComplete;
const navStub = sinon.stub(GerritNav, 'navigateToDiff');
// Noop when there are no files.
- element._openSelectedFile();
+ element.openSelectedFile();
assert.isFalse(navStub.called);
- element.set('_filesByPath', _filesByPath);
- await flush();
+ element.filesByPath = filesByPath;
+ await element.updateComplete;
// Navigates when a file is selected.
- element._openSelectedFile();
+ element.openSelectedFile();
assert.isTrue(navStub.called);
});
- test('_displayLine', () => {
+ test('displayLine', () => {
element.filesExpanded = FilesExpandedState.ALL;
- element._displayLine = false;
- element._handleCursorNext(new KeyboardEvent('keydown'));
- assert.isTrue(element._displayLine);
+ element.displayLine = false;
+ element.handleCursorNext(new KeyboardEvent('keydown'));
+ assert.isTrue(element.displayLine);
- element._displayLine = false;
- element._handleCursorPrev(new KeyboardEvent('keydown'));
- assert.isTrue(element._displayLine);
+ element.displayLine = false;
+ element.handleCursorPrev(new KeyboardEvent('keydown'));
+ assert.isTrue(element.displayLine);
- element._displayLine = true;
- element._handleEscKey();
- assert.isFalse(element._displayLine);
+ element.displayLine = true;
+ element.handleEscKey();
+ assert.isFalse(element.displayLine);
});
suite('editMode behavior', () => {
@@ -2204,22 +2235,11 @@
assert.isTrue(saveReviewStub.calledOnce);
element.editMode = true;
- await flush();
+ await element.updateComplete;
MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
assert.isTrue(saveReviewStub.calledOnce);
});
-
- test('_getReviewedFiles does not call API', () => {
- const apiSpy = spyRestApi('getReviewedFiles');
- element.editMode = true;
- return element
- ._getReviewedFiles(0 as NumericChangeId, {patchNum: 0} as PatchRange)
- .then(files => {
- assert.equal(files!.length, 0);
- assert.isFalse(apiSpy.called);
- });
- });
});
test('editing actions', async () => {
@@ -2229,7 +2249,7 @@
);
element.editMode = true;
- await flush();
+ await element.updateComplete;
// Commit message should not have edit controls.
const editControls = Array.from(
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
index 2988bc6..6bd3f62 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.ts
@@ -27,9 +27,6 @@
DetailedLabelInfo,
} from '../../../types/common';
import {assertIsDefined, hasOwnProperty} from '../../../utils/common-util';
-import {getAppContext} from '../../../services/app-context';
-import {KnownExperimentId} from '../../../services/flags/flags';
-import {classMap} from 'lit/directives/class-map';
import {Label} from '../../../utils/label-util';
import {LabelNameToValuesMap} from '../../../api/rest-api';
@@ -68,12 +65,6 @@
@state()
private selectedValueText = 'No value selected';
- private readonly flagsService = getAppContext().flagsService;
-
- private readonly isSubmitRequirementsUiEnabled = this.flagsService.isEnabled(
- KnownExperimentId.SUBMIT_REQUIREMENTS_UI
- );
-
static override get styles() {
return [
sharedStyles,
@@ -87,8 +78,6 @@
/* We want the :hover highlight to extend to the border of the dialog. */
.labelNameCell {
padding-left: var(--label-score-padding-left, 0);
- }
- .labelNameCell.newSubmitRequirements {
width: 160px;
}
.selectedValueCell {
@@ -100,9 +89,6 @@
white-space: nowrap;
}
.selectedValueCell {
- width: 75%;
- }
- .selectedValueCell.newSubmitRequirements {
width: 52%;
}
.labelMessage {
@@ -175,13 +161,7 @@
override render() {
return html`
- <span
- class=${classMap({
- labelNameCell: true,
- newSubmitRequirements: this.isSubmitRequirementsUiEnabled,
- })}
- id="labelName"
- aria-hidden="true"
+ <span class="labelNameCell" id="labelName" aria-hidden="true"
>${this.label?.name ?? ''}</span
>
${this.renderButtonsCell()} ${this.renderSelectedValue()}
@@ -257,12 +237,7 @@
private renderSelectedValue() {
return html`
- <div
- class=${classMap({
- selectedValueCell: true,
- newSubmitRequirements: this.isSubmitRequirementsUiEnabled,
- })}
- >
+ <div class="selectedValueCell">
<span id="selectedValueLabel">${this.selectedValueText}</span>
</div>
`;
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 27c445e..8e757da 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
@@ -24,10 +24,8 @@
LabelNameToValueMap,
} from '../../../types/common';
import {GrLabelScoreRow} from '../gr-label-score-row/gr-label-score-row';
-import {getAppContext} from '../../../services/app-context';
import {
getTriggerVotes,
- showNewSubmitRequirements,
computeLabels,
Label,
computeOrderedLabelValues,
@@ -48,8 +46,6 @@
@property({type: Object})
account?: AccountInfo;
- private readonly flagsService = getAppContext().flagsService;
-
static override get styles() {
return [
fontStyles,
@@ -57,8 +53,6 @@
.scoresTable {
display: table;
width: 100%;
- }
- .scoresTable.newSubmitRequirements {
table-layout: fixed;
}
.mergedMessage,
@@ -91,19 +85,6 @@
}
override render() {
- if (showNewSubmitRequirements(this.flagsService, this.change)) {
- return this.renderNewSubmitRequirements();
- } else {
- return this.renderOldSubmitRequirements();
- }
- }
-
- private renderOldSubmitRequirements() {
- const labels = computeLabels(this.account, this.change);
- return html`${this.renderLabels(labels)}${this.renderErrorMessages()}`;
- }
-
- private renderNewSubmitRequirements() {
return html`${this.renderSubmitReqsLabels()}${this.renderTriggerVotes()}
${this.renderErrorMessages()}`;
}
@@ -145,13 +126,7 @@
}
private renderLabels(labels: Label[]) {
- const newSubReqs = showNewSubmitRequirements(
- this.flagsService,
- this.change
- );
- return html`<div
- class="scoresTable ${newSubReqs ? 'newSubmitRequirements' : ''}"
- >
+ return html`<div class="scoresTable">
${labels
.filter(
label =>
diff --git a/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts b/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts
index 4bf9d10..f204d76 100644
--- a/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts
+++ b/polygerrit-ui/app/elements/change/gr-message-scores/gr-message-scores.ts
@@ -25,8 +25,6 @@
} from '../../../utils/comment-util';
import {hasOwnProperty} from '../../../utils/common-util';
import {getTriggerVotes} from '../../../utils/label-util';
-import {getAppContext} from '../../../services/app-context';
-import {KnownExperimentId} from '../../../services/flags/flags';
const VOTE_RESET_TEXT = '0 (vote reset)';
@@ -102,8 +100,6 @@
`;
}
- private readonly flagsService = getAppContext().flagsService;
-
override render() {
const scores = this._getScores(this.message, this.labelExtremes);
const triggerVotes = getTriggerVotes(this.change);
@@ -112,7 +108,6 @@
private renderScore(score: Score, triggerVotes: string[]) {
if (
- this.flagsService.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI) &&
score.label &&
triggerVotes.includes(score.label) &&
!score.value?.includes(VOTE_RESET_TEXT)
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 f6b0ad4..1af37b3 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,14 +14,11 @@
* 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 {htmlTemplate} from './gr-messages-list_html';
import {
Shortcut,
ShortcutSection,
@@ -29,7 +26,7 @@
import {parseDate} from '../../../utils/date-util';
import {MessageTag} from '../../../constants/constants';
import {getAppContext} from '../../../services/app-context';
-import {customElement, property} from '@polymer/decorators';
+import {customElement, property, state} from 'lit/decorators';
import {
ChangeId,
ChangeMessageId,
@@ -38,17 +35,10 @@
NumericChangeId,
PatchSetNum,
RepoName,
- ReviewerUpdateInfo,
VotingRangeInfo,
} from '../../../types/common';
-import {
- CommentThread,
- isRobot,
- LabelExtreme,
-} from '../../../utils/comment-util';
+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,
@@ -56,8 +46,14 @@
} from '../../../types/types';
import {commentsModelToken} from '../../../models/comments/comments-model';
import {changeModelToken} from '../../../models/change/change-model';
-import {resolve, DIPolymerElement} from '../../../models/dependency';
+import {resolve} from '../../../models/dependency';
import {queryAll} from '../../../utils/common-util';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {subscribe} from '../../lit/subscription-controller';
+import {paperStyles} from '../../../styles/gr-paper-styles';
+import {when} from 'lit/directives/when';
+import {ifDefined} from 'lit/directives/if-defined';
/**
* The content of the enum is also used in the UI for the button text.
@@ -168,6 +164,63 @@
}
/**
+ * Merges change messages and reviewer updates into one array. Also processes
+ * all messages and updates, aligns or massages some of the properties.
+ */
+function computeCombinedMessages(
+ messages: ChangeMessageInfo[],
+ reviewerUpdates: FormattedReviewerUpdateInfo[],
+ commentThreads: CommentThread[]
+): CombinedMessage[] {
+ let mi = 0;
+ let ri = 0;
+ let combinedMessages: CombinedMessage[] = [];
+ let mDate;
+ let rDate;
+ for (let i = 0; i < messages.length; i++) {
+ // TODO(TS): clone message instead and avoid API object mutation
+ (messages[i] as CombinedMessage)._index = i;
+ }
+
+ while (mi < messages.length || ri < reviewerUpdates.length) {
+ if (mi >= messages.length) {
+ combinedMessages = combinedMessages.concat(reviewerUpdates.slice(ri));
+ break;
+ }
+ if (ri >= reviewerUpdates.length) {
+ combinedMessages = combinedMessages.concat(messages.slice(mi));
+ break;
+ }
+ mDate = mDate || parseDate(messages[mi].date);
+ rDate = rDate || parseDate(reviewerUpdates[ri].date);
+ if (rDate < mDate) {
+ combinedMessages.push(reviewerUpdates[ri++]);
+ rDate = null;
+ } else {
+ combinedMessages.push(messages[mi++]);
+ mDate = null;
+ }
+ }
+
+ for (let i = 0; i < combinedMessages.length; i++) {
+ const message = combinedMessages[i];
+ if (message.expanded === undefined) {
+ message.expanded = false;
+ }
+ message.commentThreads = computeThreads(message, commentThreads);
+ message._revision_number = computeRevision(message, combinedMessages);
+ message.tag = computeTag(message);
+ }
+ // computeIsImportant() depends on tags and revision numbers already being
+ // updated for all messages, so we have to compute this in its own forEach
+ // loop.
+ combinedMessages.forEach(m => {
+ m.isImportant = computeIsImportant(m, combinedMessages);
+ });
+ return combinedMessages;
+}
+
+/**
* Unimportant messages are initially hidden.
*
* Human messages are always important. They have an undefined tag.
@@ -194,69 +247,81 @@
computeIsImportant,
};
-export interface GrMessagesList {
- $: {
- messageRepeat: DomRepeat;
- };
-}
-
@customElement('gr-messages-list')
-export class GrMessagesList extends DIPolymerElement {
- static get template() {
- return htmlTemplate;
+export class GrMessagesList extends LitElement {
+ // TODO: Evaluate if we still need to have display: flex on the :host and
+ // .header.
+ static override get styles() {
+ return [
+ sharedStyles,
+ paperStyles,
+ css`
+ :host {
+ display: flex;
+ justify-content: space-between;
+ }
+ .header {
+ align-items: center;
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ justify-content: space-between;
+ padding: var(--spacing-s) var(--spacing-l);
+ }
+ .highlighted {
+ animation: 3s fadeOut;
+ }
+ @keyframes fadeOut {
+ 0% {
+ background-color: var(--emphasis-color);
+ }
+ 100% {
+ background-color: var(--view-background-color);
+ }
+ }
+ .container {
+ align-items: center;
+ display: flex;
+ }
+ .hiddenEntries {
+ color: var(--deemphasized-text-color);
+ }
+ gr-message:not(:last-of-type) {
+ border-bottom: 1px solid var(--border-color);
+ }
+ `,
+ ];
}
- // Private internal @state, derived from the application state.
- @property({type: Object})
- change?: ParsedChangeInfo;
-
- // Private internal @state, derived from the application state.
- @property({type: String})
- changeNum?: ChangeId | NumericChangeId;
-
@property({type: Array})
messages: ChangeMessageInfo[] = [];
@property({type: Array})
- reviewerUpdates: ReviewerUpdateInfo[] = [];
-
- // Private internal @state, derived from the application state.
- @property({type: Object})
- 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;
+ reviewerUpdates: FormattedReviewerUpdateInfo[] = [];
@property({type: Object})
labels?: LabelNameToInfoMap;
- @property({type: String})
- _expandAllState = ExpandAllState.EXPAND_ALL;
+ @state()
+ private change?: ParsedChangeInfo;
- @property({type: String, computed: '_computeExpandAllTitle(_expandAllState)'})
- _expandAllTitle = '';
+ @state()
+ private changeNum?: ChangeId | NumericChangeId;
- @property({type: Boolean, observer: '_observeShowAllActivity'})
- _showAllActivity = false;
+ @state()
+ private commentThreads: CommentThread[] = [];
- @property({
- type: Array,
- computed:
- '_computeCombinedMessages(messages, reviewerUpdates, ' +
- 'commentThreads)',
- observer: '_combinedMessagesChanged',
- })
- _combinedMessages: CombinedMessage[] = [];
+ @state()
+ private projectName?: RepoName;
- @property({type: Object, computed: '_computeLabelExtremes(labels.*)'})
- _labelExtremes: LabelExtreme = {};
+ @state()
+ expandAllState = ExpandAllState.EXPAND_ALL;
- private readonly userModel = getAppContext().userModel;
+ // Private but used in tests.
+ @state()
+ showAllActivity = false;
+
+ @state()
+ private combinedMessages: CombinedMessage[] = [];
// Private but used in tests.
readonly getCommentsModel = resolve(this, commentsModelToken);
@@ -267,43 +332,106 @@
private readonly shortcuts = getAppContext().shortcutsService;
- private subscriptions: Subscription[] = [];
-
- override connectedCallback() {
- super.connectedCallback();
- this.subscriptions.push(
- this.getCommentsModel().threads$.subscribe(x => {
+ constructor() {
+ super();
+ subscribe(
+ this,
+ () => this.getCommentsModel().threads$,
+ x => {
this.commentThreads = x;
- })
+ }
);
- this.subscriptions.push(
- this.changeModel().change$.subscribe(x => {
+ subscribe(
+ this,
+ () => this.changeModel().change$,
+ x => {
this.change = x;
- })
+ }
);
- this.subscriptions.push(
- this.userModel.loggedIn$.subscribe(x => {
- this.showReplyButtons = x;
- })
- );
- this.subscriptions.push(
- this.changeModel().repo$.subscribe(x => {
+ subscribe(
+ this,
+ () => this.changeModel().repo$,
+ x => {
this.projectName = x;
- })
+ }
);
- this.subscriptions.push(
- this.changeModel().changeNum$.subscribe(x => {
+ subscribe(
+ this,
+ () => this.changeModel().changeNum$,
+ x => {
this.changeNum = x;
- })
+ }
);
}
- override disconnectedCallback() {
- for (const s of this.subscriptions) {
- s.unsubscribe();
+ override willUpdate(changedProperties: PropertyValues): void {
+ if (
+ changedProperties.has('messages') ||
+ changedProperties.has('reviewerUpdates') ||
+ changedProperties.has('commentThreads')
+ ) {
+ this.combinedMessages = computeCombinedMessages(
+ this.messages,
+ this.reviewerUpdates,
+ this.commentThreads
+ );
+ this.combinedMessagesChanged();
}
- this.subscriptions = [];
- super.disconnectedCallback();
+ }
+
+ override render() {
+ const labelExtremes = this.computeLabelExtremes();
+ return html`${this.renderHeader()}
+ ${this.combinedMessages
+ .filter(m => this.showAllActivity || m.isImportant)
+ .map(
+ message => html`<gr-message
+ .change=${this.change}
+ .changeNum=${this.changeNum}
+ .message=${message}
+ .commentThreads=${message.commentThreads}
+ .projectName=${this.projectName}
+ @message-anchor-tap=${this.handleAnchorClick}
+ .labelExtremes=${labelExtremes}
+ data-message-id=${ifDefined(getMessageId(message) as String)}
+ ></gr-message>`
+ )}`;
+ }
+
+ private renderHeader() {
+ return html`<div class="header">
+ <div id="showAllActivityToggleContainer" class="container">
+ ${when(
+ this.combinedMessages.some(m => !m.isImportant),
+ () => html`
+ <paper-toggle-button
+ class="showAllActivityToggle"
+ ?checked=${this.showAllActivity}
+ @change=${this.handleShowAllActivityChanged}
+ aria-labelledby="showAllEntriesLabel"
+ role="switch"
+ @click=${this.onTapShowAllActivityToggle}
+ ></paper-toggle-button>
+ <div id="showAllEntriesLabel" aria-hidden="true">
+ <span>Show all entries</span>
+ <span class="hiddenEntries" ?hidden=${this.showAllActivity}>
+ (${this.combinedMessages.filter(m => !m.isImportant).length}
+ hidden)
+ </span>
+ </div>
+ <span class="transparent separator"></span>
+ `
+ )}
+ </div>
+ <gr-button
+ id="collapse-messages"
+ link
+ .title=${this.computeExpandAllTitle()}
+ @click=${this.handleExpandCollapseTap}
+ >
+ ${this.expandAllState}
+ </gr-button>
+ </div>`;
}
async scrollToMessage(messageID: string) {
@@ -312,14 +440,14 @@
| GrMessage
| undefined;
- if (!el && this._showAllActivity) {
+ if (!el && this.showAllActivity) {
this.reporting.error(
new Error(`Failed to scroll to message: ${messageID}`)
);
return;
}
if (!el || !el.message) {
- this._showAllActivity = true;
+ this.showAllActivity = true;
setTimeout(() => this.scrollToMessage(messageID));
return;
}
@@ -336,101 +464,27 @@
top += offsetParent.offsetTop;
}
window.scrollTo(0, top);
- this._highlightEl(el);
+ this.highlightEl(el);
}
- _observeShowAllActivity() {
- // We have to call render() such that the dom-repeat filter picks up the
- // change.
- this.$.messageRepeat.render();
+ private handleShowAllActivityChanged(e: Event) {
+ this.showAllActivity = (e.target as HTMLInputElement).checked ?? false;
}
- /**
- * Filter for the dom-repeat of combinedMessages.
- */
- _isMessageVisible(message: CombinedMessage) {
- return this._showAllActivity || message.isImportant;
- }
-
- /**
- * Merges change messages and reviewer updates into one array. Also processes
- * all messages and updates, aligns or massages some of the properties.
- */
- _computeCombinedMessages(
- messages: ChangeMessageInfo[] | undefined,
- reviewerUpdates: FormattedReviewerUpdateInfo[] | undefined,
- commentThreads: CommentThread[]
- ) {
- if (messages === undefined || reviewerUpdates === undefined) return;
-
- let mi = 0;
- let ri = 0;
- let combinedMessages: CombinedMessage[] = [];
- let mDate;
- let rDate;
- for (let i = 0; i < messages.length; i++) {
- // TODO(TS): clone message instead and avoid API object mutation
- (messages[i] as CombinedMessage)._index = i;
- }
-
- while (mi < messages.length || ri < reviewerUpdates.length) {
- if (mi >= messages.length) {
- combinedMessages = combinedMessages.concat(reviewerUpdates.slice(ri));
- break;
- }
- if (ri >= reviewerUpdates.length) {
- combinedMessages = combinedMessages.concat(messages.slice(mi));
- break;
- }
- mDate = mDate || parseDate(messages[mi].date);
- rDate = rDate || parseDate(reviewerUpdates[ri].date);
- if (rDate < mDate) {
- combinedMessages.push(reviewerUpdates[ri++]);
- rDate = null;
- } else {
- combinedMessages.push(messages[mi++]);
- mDate = null;
- }
- }
-
- for (let i = 0; i < combinedMessages.length; i++) {
- const message = combinedMessages[i];
- if (message.expanded === undefined) {
- message.expanded = false;
- }
- message.commentThreads = computeThreads(message, commentThreads);
- message._revision_number = computeRevision(message, combinedMessages);
- message.tag = computeTag(message);
- }
- // computeIsImportant() depends on tags and revision numbers already being
- // updated for all messages, so we have to compute this in its own forEach
- // loop.
- combinedMessages.forEach(m => {
- m.isImportant = computeIsImportant(m, combinedMessages);
- });
- return combinedMessages;
- }
-
- _updateExpandedStateOfAllMessages(exp: boolean) {
- if (!this._combinedMessages) return;
-
- for (let i = 0; i < this._combinedMessages.length; i++) {
- this._combinedMessages[i].expanded = exp;
- this.notifyPath(`_combinedMessages.${i}.expanded`);
- }
+ private refreshMessages() {
for (const message of queryAll<GrMessage>(this, 'gr-message')) {
- message.requestUpdate('message');
+ message.requestUpdate();
}
}
- _computeExpandAllTitle(_expandAllState?: string) {
- if (_expandAllState === ExpandAllState.COLLAPSE_ALL) {
+ private computeExpandAllTitle() {
+ if (this.expandAllState === ExpandAllState.COLLAPSE_ALL) {
return this.shortcuts.createTitle(
Shortcut.COLLAPSE_ALL_MESSAGES,
ShortcutSection.ACTIONS
);
}
- if (_expandAllState === ExpandAllState.EXPAND_ALL) {
+ if (this.expandAllState === ExpandAllState.EXPAND_ALL) {
return this.shortcuts.createTitle(
Shortcut.EXPAND_ALL_MESSAGES,
ShortcutSection.ACTIONS
@@ -439,8 +493,10 @@
return '';
}
- _highlightEl(el: HTMLElement) {
- const highlightedEls = this.root!.querySelectorAll('.highlighted');
+ // Private but used in tests.
+ highlightEl(el: HTMLElement) {
+ const highlightedEls =
+ this.shadowRoot?.querySelectorAll('.highlighted') ?? [];
for (const highlightedEl of highlightedEls) {
highlightedEl.classList.remove('highlighted');
}
@@ -452,42 +508,36 @@
el.classList.add('highlighted');
}
+ // Private but used in tests.
handleExpandCollapse(expand: boolean) {
- this._expandAllState = expand
+ this.expandAllState = expand
? ExpandAllState.COLLAPSE_ALL
: ExpandAllState.EXPAND_ALL;
- this._updateExpandedStateOfAllMessages(expand);
+ if (!this.combinedMessages) return;
+ for (let i = 0; i < this.combinedMessages.length; i++) {
+ this.combinedMessages[i].expanded = expand;
+ }
+ this.refreshMessages();
}
- _handleExpandCollapseTap(e: Event) {
+ private handleExpandCollapseTap(e: Event) {
e.preventDefault();
this.handleExpandCollapse(
- this._expandAllState === ExpandAllState.EXPAND_ALL
+ this.expandAllState === ExpandAllState.EXPAND_ALL
);
}
- _handleAnchorClick(e: CustomEvent<MessageAnchorTapDetail>) {
+ private handleAnchorClick(e: CustomEvent<MessageAnchorTapDetail>) {
this.scrollToMessage(e.detail.id);
}
- _isVisibleShowAllActivityToggle(messages: CombinedMessage[] = []) {
- return messages.some(m => !m.isImportant);
- }
-
- _computeHiddenEntriesCount(messages: CombinedMessage[] = []) {
- return messages.filter(m => !m.isImportant).length;
- }
-
/**
- * Called when this._combinedMessages has changed.
+ * Called when this.combinedMessages has changed.
*/
- _combinedMessagesChanged(combinedMessages?: CombinedMessage[]) {
- if (!combinedMessages) return;
- if (combinedMessages.length === 0) return;
- for (let i = 0; i < combinedMessages.length; i++) {
- this.notifyPath(`_combinedMessages.${i}.commentThreads`);
- }
- const tags = combinedMessages.map(
+ private combinedMessagesChanged() {
+ if (this.combinedMessages.length === 0) return;
+ this.refreshMessages();
+ const tags = this.combinedMessages.map(
message =>
message.tag || (message as FormattedReviewerUpdateInfo).type || 'none'
);
@@ -496,7 +546,7 @@
acc[val] = (acc[val] || 0) + 1;
return acc;
},
- {all: combinedMessages.length} as TagsCountReportInfo
+ {all: this.combinedMessages.length} as TagsCountReportInfo
);
this.reporting.reportInteraction('messages-count', tagsCounted);
}
@@ -504,20 +554,15 @@
/**
* Compute a mapping from label name to objects representing the minimum and
* maximum possible values for that label.
+ * Private but used in tests.
*/
- _computeLabelExtremes(
- labelRecord: PolymerDeepPropertyChange<
- LabelNameToInfoMap,
- LabelNameToInfoMap
- >
- ) {
+ computeLabelExtremes() {
const extremes: {[labelName: string]: VotingRangeInfo} = {};
- const labels = labelRecord.base;
- if (!labels) {
+ if (!this.labels) {
return extremes;
}
- for (const key of Object.keys(labels)) {
- const range = getVotingRange(labels[key]);
+ for (const key of Object.keys(this.labels)) {
+ const range = getVotingRange(this.labels[key]);
if (range) {
extremes[key] = range;
}
@@ -528,7 +573,7 @@
/**
* Work around a issue on iOS when clicking turns into double tap
*/
- _onTapShowAllActivityToggle(e: Event) {
+ private onTapShowAllActivityToggle(e: Event) {
e.preventDefault();
}
}
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
deleted file mode 100644
index 087ee19..0000000
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.ts
+++ /dev/null
@@ -1,107 +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: flex;
- justify-content: space-between;
- }
- .header {
- align-items: center;
- border-bottom: 1px solid var(--border-color);
- display: flex;
- justify-content: space-between;
- padding: var(--spacing-s) var(--spacing-l);
- }
- .highlighted {
- animation: 3s fadeOut;
- }
- @keyframes fadeOut {
- 0% {
- background-color: var(--emphasis-color);
- }
- 100% {
- background-color: var(--view-background-color);
- }
- }
- .container {
- align-items: center;
- display: flex;
- }
- .hiddenEntries {
- color: var(--deemphasized-text-color);
- }
- gr-message:not(:last-of-type) {
- 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
- is="dom-if"
- if="[[_isVisibleShowAllActivityToggle(_combinedMessages)]]"
- >
- <paper-toggle-button
- class="showAllActivityToggle"
- checked="{{_showAllActivity}}"
- aria-labelledby="showAllEntriesLabel"
- role="switch"
- on-click="_onTapShowAllActivityToggle"
- ></paper-toggle-button>
- <div id="showAllEntriesLabel" aria-hidden="true">
- <span>Show all entries</span>
- <span class="hiddenEntries" hidden$="[[_showAllActivity]]">
- ([[_computeHiddenEntriesCount(_combinedMessages)]] hidden)
- </span>
- </div>
- <span class="transparent separator"></span>
- </template>
- </div>
- <gr-button
- id="collapse-messages"
- link=""
- title="[[_expandAllTitle]]"
- on-click="_handleExpandCollapseTap"
- >
- [[_expandAllState]]
- </gr-button>
- </div>
- <template
- id="messageRepeat"
- is="dom-repeat"
- items="[[_combinedMessages]]"
- as="message"
- filter="_isMessageVisible"
- >
- <gr-message
- change="[[change]]"
- change-num="[[changeNum]]"
- message="[[message]]"
- comment-threads="[[message.commentThreads]]"
- project-name="[[projectName]]"
- show-reply-button="[[showReplyButtons]]"
- on-message-anchor-tap="_handleAnchorClick"
- label-extremes="[[_labelExtremes]]"
- data-message-id$="[[message.id]]"
- ></gr-message>
- </template>
-`;
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
index b9cb616..8802ff3 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.ts
@@ -17,10 +17,8 @@
import '../../../test/common-test-setup-karma';
import './gr-messages-list';
-import {createCommentApiMockWithTemplateElement} from '../../../test/mocks/comment-api';
import {CombinedMessage, GrMessagesList, TEST_ONLY} from './gr-messages-list';
import {MessageTag} from '../../../constants/constants';
-import {html} from '@polymer/polymer/lib/utils/html-tag';
import {
query,
queryAll,
@@ -42,17 +40,8 @@
} from '../../../types/common';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import {assertIsDefined} from '../../../utils/common-util';
-
-createCommentApiMockWithTemplateElement(
- 'gr-messages-list-comment-mock-api',
- html` <gr-messages-list id="messagesList"></gr-messages-list> `
-);
-
-const basicFixture = fixtureFromTemplate(html`
- <gr-messages-list-comment-mock-api>
- <gr-messages-list></gr-messages-list>
- </gr-messages-list-comment-mock-api>
-`);
+import {html} from 'lit';
+import {fixture} from '@open-wc/testing-helpers';
const author = {
_account_id: 42 as AccountId,
@@ -99,8 +88,6 @@
let element: GrMessagesList;
let messages: ChangeMessageInfo[];
- let commentApiWrapper: any;
-
const getMessages = function () {
return queryAll<GrMessage>(element, 'gr-message');
};
@@ -156,16 +143,12 @@
stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
messages = generateRandomMessages(3);
- // Element must be wrapped in an element with direct access to the
- // comment API.
- commentApiWrapper = basicFixture.instantiate();
- element = queryAndAssert<GrMessagesList>(
- commentApiWrapper,
- '#messagesList'
+ element = await fixture<GrMessagesList>(
+ html`<gr-messages-list></gr-messages-list>`
);
await element.getCommentsModel().reloadComments(0 as NumericChangeId);
element.messages = messages;
- await flush();
+ await element.updateComplete;
});
test('expand/collapse all', async () => {
@@ -176,15 +159,18 @@
await message.updateComplete;
}
MockInteractions.tap(allMessageEls[1]);
+ await element.updateComplete;
assert.isTrue(allMessageEls[1].message?.expanded);
MockInteractions.tap(queryAndAssert(element, '#collapse-messages'));
+ await element.updateComplete;
allMessageEls = getMessages();
for (const message of allMessageEls) {
assert.isTrue(message.message?.expanded);
}
MockInteractions.tap(queryAndAssert(element, '#collapse-messages'));
+ await element.updateComplete;
allMessageEls = getMessages();
for (const message of allMessageEls) {
assert.isFalse(message.message?.expanded);
@@ -230,7 +216,7 @@
}
const scrollToStub = sinon.stub(window, 'scrollTo');
- const highlightStub = sinon.stub(element, '_highlightEl');
+ const highlightStub = sinon.stub(element, 'highlightEl');
await element.scrollToMessage('invalid');
@@ -255,7 +241,7 @@
test('scroll to message offscreen', async () => {
const scrollToStub = sinon.stub(window, 'scrollTo');
- const highlightStub = sinon.stub(element, '_highlightEl');
+ const highlightStub = sinon.stub(element, 'highlightEl');
element.messages = generateRandomMessages(25);
await element.updateComplete;
assert.isFalse(scrollToStub.called);
@@ -271,7 +257,7 @@
);
});
- test('associating messages with comments', () => {
+ test('associating messages with comments', async () => {
// Have to type as any otherwise fails with
// Argument of type 'ChangeMessageInfo[]' is not assignable to
// parameter of type 'ConcatArray<never>'.
@@ -295,14 +281,14 @@
} as CombinedMessage
);
element.messages = messages;
- flush();
+ await element.updateComplete;
const messageElements = getMessages();
assert.equal(messageElements.length, messages.length);
assert.deepEqual(messageElements[1].message, messages[1]);
assert.deepEqual(messageElements[2].message, messages[2]);
});
- test('threads', () => {
+ test('threads', async () => {
const messages = [
{
_index: 5,
@@ -314,7 +300,7 @@
},
];
element.messages = messages;
- flush();
+ await element.updateComplete;
const messageElements = getMessages();
// threads
assert.equal(messageElements[0].message!.commentThreads.length, 3);
@@ -468,7 +454,7 @@
assert.isFalse(TEST_ONLY.computeIsImportant(m3, [m1, m2, m3]));
});
- test('isImportant is evaluated after tag update', () => {
+ test('isImportant is evaluated after tag update', async () => {
const m1 = randomMessage({
...randomMessage(),
tag: MessageTag.TAG_NEW_PATCHSET as ReviewInputTag,
@@ -480,12 +466,12 @@
_revision_number: 2 as PatchSetNum,
});
element.messages = [m1, m2];
- flush();
+ await element.updateComplete;
assert.isFalse((m1 as CombinedMessage).isImportant);
assert.isTrue((m2 as CombinedMessage).isImportant);
});
- test('messages without author do not throw', () => {
+ test('messages without author do not throw', async () => {
const messages = [
{
_index: 5,
@@ -496,7 +482,7 @@
},
];
element.messages = messages;
- flush();
+ await element.updateComplete;
const messageEls = getMessages();
assert.equal(messageEls.length, 1);
assert.equal(messageEls[0].message!.message, messages[0].message);
@@ -507,9 +493,7 @@
let element: GrMessagesList;
let messages: ChangeMessageInfo[];
- let commentApiWrapper: any;
-
- setup(() => {
+ setup(async () => {
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
stubRestApi('getDiffComments').returns(Promise.resolve({}));
stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
@@ -529,15 +513,11 @@
}),
];
- // Element must be wrapped in an element with direct access to the
- // comment API.
- commentApiWrapper = basicFixture.instantiate();
- element = queryAndAssert<GrMessagesList>(
- commentApiWrapper,
- '#messagesList'
+ element = await fixture<GrMessagesList>(
+ html`<gr-messages-list></gr-messages-list>`
);
element.messages = messages;
- flush();
+ await element.updateComplete;
});
test('hide autogenerated button is not hidden', () => {
@@ -550,59 +530,53 @@
assert.equal(displayedMsgs.length, 2);
});
- test('unimportant messages hidden after toggle', () => {
- element._showAllActivity = true;
+ test('unimportant messages hidden after toggle', async () => {
+ element.showAllActivity = true;
+ await element.updateComplete;
const toggle = queryAndAssert(element, '.showAllActivityToggle');
assert.isOk(toggle);
MockInteractions.tap(toggle);
- flush();
+ await element.updateComplete;
const displayedMsgs = queryAll<GrMessage>(element, 'gr-message');
assert.equal(displayedMsgs.length, 2);
});
- test('unimportant messages shown after toggle', () => {
- element._showAllActivity = false;
+ test('unimportant messages shown after toggle', async () => {
+ element.showAllActivity = false;
+ await element.updateComplete;
const toggle = queryAndAssert(element, '.showAllActivityToggle');
assert.isOk(toggle);
MockInteractions.tap(toggle);
- flush();
+ await element.updateComplete;
const displayedMsgs = queryAll<GrMessage>(element, 'gr-message');
assert.equal(displayedMsgs.length, 3);
});
test('_computeLabelExtremes', () => {
- const computeSpy = sinon.spy(element, '_computeLabelExtremes');
-
// Have to type as any to be able to use null.
element.labels = null as any;
- assert.isTrue(computeSpy.calledOnce);
- assert.deepEqual(computeSpy.lastCall.returnValue, {});
+ assert.deepEqual(element.computeLabelExtremes(), {});
element.labels = {};
- assert.isTrue(computeSpy.calledTwice);
- assert.deepEqual(computeSpy.lastCall.returnValue, {});
+ assert.deepEqual(element.computeLabelExtremes(), {});
element.labels = {'my-label': {}};
- assert.isTrue(computeSpy.calledThrice);
- assert.deepEqual(computeSpy.lastCall.returnValue, {});
+ assert.deepEqual(element.computeLabelExtremes(), {});
element.labels = {'my-label': {values: {}}};
- assert.equal(computeSpy.callCount, 4);
- assert.deepEqual(computeSpy.lastCall.returnValue, {});
+ assert.deepEqual(element.computeLabelExtremes(), {});
element.labels = {
'my-label': {values: {'-12': {}}},
} as LabelNameToInfoMap;
- assert.equal(computeSpy.callCount, 5);
- assert.deepEqual(computeSpy.lastCall.returnValue, {
+ assert.deepEqual(element.computeLabelExtremes(), {
'my-label': {min: -12, max: -12},
});
element.labels = {
'my-label': {values: {'-2': {}, '-1': {}, '0': {}, '+1': {}, '+2': {}}},
} as LabelNameToInfoMap;
- assert.equal(computeSpy.callCount, 6);
- assert.deepEqual(computeSpy.lastCall.returnValue, {
+ assert.deepEqual(element.computeLabelExtremes(), {
'my-label': {min: -2, max: 2},
});
@@ -610,8 +584,7 @@
'my-label': {values: {'-12': {}}},
'other-label': {values: {'-1': {}, ' 0': {}, '+1': {}}},
} as LabelNameToInfoMap;
- assert.equal(computeSpy.callCount, 7);
- assert.deepEqual(computeSpy.lastCall.returnValue, {
+ assert.deepEqual(element.computeLabelExtremes(), {
'my-label': {min: -12, max: -12},
'other-label': {min: -1, max: 1},
});
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 1f57837..6a9bd92 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
@@ -261,7 +261,7 @@
account?: AccountInfo;
@state()
- ccs: (AccountInfoInput | GroupInfoInput)[] = [];
+ ccs: AccountInput[] = [];
@state()
attentionCcsCount = 0;
@@ -1527,8 +1527,8 @@
if (!this.change?.owner || !this.change?.reviewers) return;
this.owner = this.change.owner;
- const reviewers = [];
- const ccs = [];
+ const reviewers: AccountInput[] = [];
+ const ccs: AccountInput[] = [];
if (this.change.reviewers) {
for (const key of Object.keys(this.change.reviewers)) {
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 c800c87..8d4062e 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
@@ -193,6 +193,157 @@
return promise;
}
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `
+ <div tabindex="-1">
+ <section class="peopleContainer">
+ <gr-endpoint-decorator name="reply-reviewers">
+ <gr-endpoint-param name="change"> </gr-endpoint-param>
+ <gr-endpoint-param name="reviewers"> </gr-endpoint-param>
+ <div class="peopleList">
+ <div class="peopleListLabel">Reviewers</div>
+ <gr-account-list id="reviewers"> </gr-account-list>
+ <gr-endpoint-slot name="right"> </gr-endpoint-slot>
+ </div>
+ <gr-endpoint-slot name="below"> </gr-endpoint-slot>
+ </gr-endpoint-decorator>
+ <div class="peopleList">
+ <div class="peopleListLabel">CC</div>
+ <gr-account-list allow-any-input="" id="ccs"> </gr-account-list>
+ </div>
+ <gr-overlay
+ aria-hidden="true"
+ id="reviewerConfirmationOverlay"
+ style="outline: none; display: none;"
+ >
+ <div class="reviewerConfirmation">
+ Group
+ <span class="groupName"> </span>
+ has
+ <span class="groupSize"> </span>
+ members.
+ <br />
+ Are you sure you want to add them all?
+ </div>
+ <div class="reviewerConfirmationButtons">
+ <gr-button aria-disabled="false" role="button" tabindex="0">
+ Yes
+ </gr-button>
+ <gr-button aria-disabled="false" role="button" tabindex="0">
+ No
+ </gr-button>
+ </div>
+ </gr-overlay>
+ </section>
+ <section class="labelsContainer">
+ <gr-endpoint-decorator name="reply-label-scores">
+ <gr-label-scores id="labelScores"> </gr-label-scores>
+ <gr-endpoint-param name="change"> </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ <div id="pluginMessage"></div>
+ </section>
+ <section class="newReplyDialog textareaContainer">
+ <div class="patchsetLevelContainer resolved">
+ <gr-endpoint-decorator name="reply-text">
+ <gr-textarea
+ class="message monospace newReplyDialog"
+ id="textarea"
+ monospace=""
+ >
+ </gr-textarea>
+ <gr-endpoint-param name="change"> </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ <div class="labelContainer">
+ <label>
+ <input
+ checked=""
+ id="resolvedPatchsetLevelCommentCheckbox"
+ type="checkbox"
+ />
+ Resolved
+ </label>
+ <label class="preview-formatting">
+ <input type="checkbox" />
+ Preview formatting
+ </label>
+ </div>
+ </div>
+ </section>
+ <div class="newReplyDialog stickyBottom">
+ <gr-endpoint-decorator name="reply-bottom">
+ <gr-endpoint-param name="change"> </gr-endpoint-param>
+ <section class="attention">
+ <div class="attentionSummary">
+ <div>
+ <span> No changes to the attention set. </span>
+ <gr-tooltip-content
+ has-tooltip=""
+ title="Edit attention set changes"
+ >
+ <gr-button
+ aria-disabled="false"
+ class="edit-attention-button"
+ data-action-key="edit"
+ data-action-type="change"
+ data-label="Edit"
+ link=""
+ position-below=""
+ 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>
+ </section>
+ <gr-endpoint-slot name="above-actions"> </gr-endpoint-slot>
+ <section class="actions">
+ <div class="left"></div>
+ <div class="right">
+ <gr-button
+ aria-disabled="false"
+ class="action cancel"
+ id="cancelButton"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ Cancel
+ </gr-button>
+ <gr-tooltip-content has-tooltip="" title="Send reply">
+ <gr-button
+ aria-disabled="false"
+ class="action send"
+ id="sendButton"
+ primary=""
+ role="button"
+ tabindex="0"
+ >
+ Send
+ </gr-button>
+ </gr-tooltip-content>
+ </div>
+ </section>
+ </gr-endpoint-decorator>
+ </div>
+ </div>
+ `);
+ });
+
test('default to publishing draft comments with reply', async () => {
// Async tick is needed because iron-selector content is distributed and
// distributed content requires an observer to be set up.
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 6740977..d5ce654 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
@@ -29,12 +29,7 @@
LabelInfo,
} from '../../../types/common';
import {hasOwnProperty} from '../../../utils/common-util';
-import {getAppContext} from '../../../services/app-context';
-import {
- getApprovalInfo,
- getCodeReviewLabel,
- showNewSubmitRequirements,
-} from '../../../utils/label-util';
+import {getApprovalInfo, getCodeReviewLabel} from '../../../utils/label-util';
import {sortReviewers} from '../../../utils/attention-set-util';
import {sharedStyles} from '../../../styles/shared-styles';
import {css} from 'lit';
@@ -68,8 +63,6 @@
@state() showAllReviewers = false;
- private readonly flagsService = getAppContext().flagsService;
-
static override get styles() {
return [
sharedStyles,
@@ -166,14 +159,12 @@
.vote=${this.computeVote(reviewer)}
.label=${this.computeCodeReviewLabel()}
>
- ${showNewSubmitRequirements(this.flagsService, this.change)
- ? html`<gr-vote-chip
- slot="vote-chip"
- .vote=${this.computeVote(reviewer)}
- .label=${this.computeCodeReviewLabel()}
- circle-shape
- ></gr-vote-chip>`
- : nothing}
+ <gr-vote-chip
+ slot="vote-chip"
+ .vote=${this.computeVote(reviewer)}
+ .label=${this.computeCodeReviewLabel()}
+ circle-shape
+ ></gr-vote-chip>
</gr-account-chip>
`;
}
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 eb6d071..9e4bf3f 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
@@ -116,7 +116,8 @@
return !!(diff.binary && (isA || isB));
}
-interface LineInfo {
+// visible for testing
+export interface LineInfo {
beforeNumber?: LineNumber;
afterNumber?: LineNumber;
}
@@ -277,15 +278,18 @@
private readonly getChecksModel = resolve(this, checksModelToken);
- private readonly reporting = getAppContext().reportingService;
+ // visible for testing
+ readonly reporting = getAppContext().reportingService;
private readonly flags = getAppContext().flagsService;
private readonly restApiService = getAppContext().restApiService;
- private readonly jsAPI = getAppContext().jsApiService;
+ // visible for testing
+ readonly jsAPI = getAppContext().jsApiService;
- private readonly syntaxLayer: GrSyntaxLayerWorker;
+ // visible for testing
+ readonly syntaxLayer: GrSyntaxLayerWorker;
private checksSubscription?: Subscription;
@@ -315,13 +319,6 @@
);
}
- override ready() {
- super.ready();
- if (this._canReload()) {
- this.reload();
- }
- }
-
override connectedCallback() {
super.connectedCallback();
this.subscriptions.push(
@@ -390,7 +387,6 @@
// assets in parallel.
const layerPromise = this.initLayers();
const diff = await this._getDiff();
- this.subscribeToChecks();
this._loadedWhitespaceLevel = whitespaceLevel;
this._reportDiff(diff);
@@ -408,8 +404,10 @@
this.reporting.timeEnd(Timing.DIFF_LOAD, this.timingDetails());
this.reporting.time(Timing.DIFF_CONTENT);
+
const syntaxLayerPromise = this.syntaxLayer.process(diff);
await waitForEventOnce(this, 'render');
+ this.subscribeToChecks();
this.reporting.timeEnd(Timing.DIFF_CONTENT, this.timingDetails());
if (shouldReportMetric) {
@@ -752,12 +750,6 @@
return this.restApiService.getLoggedIn();
}
- _canReload() {
- return (
- !!this.changeNum && !!this.patchRange && !!this.path && !this.noAutoRender
- );
- }
-
// TODO(milutin): Use rest-api with fetchCacheURL instead of this.
prefetchDiff() {
if (
@@ -1162,7 +1154,9 @@
if (prefsChangeRecord === undefined) return;
if (prefsChangeRecord.path !== 'prefs.syntax_highlighting') return;
- if (!noRenderOnPrefsChange) this.reload();
+ if (!noRenderOnPrefsChange) {
+ this.reload();
+ }
}
_computeParentIndex(
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
deleted file mode 100644
index dda8490..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
+++ /dev/null
@@ -1,1587 +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-diff-host.js';
-
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {createDefaultDiffPrefs, Side} from '../../../constants/constants.js';
-import {createChange, createComment, createCommentThread} from '../../../test/test-data-generators.js';
-import {addListenerForTest, mockPromise, stubRestApi, waitUntil} from '../../../test/test-utils.js';
-import {EditPatchSetNum, ParentPatchSetNum} from '../../../types/common.js';
-import {CoverageType} from '../../../types/types.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-import {GrDiffBuilderImage} from '../../../embed/diff/gr-diff-builder/gr-diff-builder-image.js';
-import {waitForEventOnce} from '../../../utils/event-util.js';
-
-const basicFixture = fixtureFromElement('gr-diff-host');
-
-suite('gr-diff-host tests', () => {
- let element;
-
- let loggedIn;
-
- setup(async () => {
- loggedIn = false;
- stubRestApi('getLoggedIn').callsFake(() => Promise.resolve(loggedIn));
- element = basicFixture.instantiate();
- element.changeNum = 123;
- element.path = 'some/path';
- sinon.stub(element.reporting, 'time');
- sinon.stub(element.reporting, 'timeEnd');
- await flush();
- });
-
- suite('plugin layers', () => {
- const pluginLayers = [{annotate: () => {}}, {annotate: () => {}}];
- setup(() => {
- element = basicFixture.instantiate();
- sinon.stub(element.jsAPI, 'getDiffLayers').returns(pluginLayers);
- element.changeNum = 123;
- element.path = 'some/path';
- });
- test('plugin layers requested', async () => {
- element.patchRange = {};
- element.change = createChange();
- stubRestApi('getDiff').returns(Promise.resolve({content: []}));
- await element.reload();
- assert(element.jsAPI.getDiffLayers.called);
- });
- });
-
- suite('render reporting', () => {
- test('ends total and syntax timer after syntax layer', async () => {
- sinon.stub(element.reporting, 'diffViewContentDisplayed');
- let notifySyntaxProcessed;
- sinon.stub(element.syntaxLayer, 'process').returns(
- new Promise(resolve => {
- notifySyntaxProcessed = resolve;
- })
- );
- stubRestApi('getDiff').returns(Promise.resolve({content: []}));
- element.patchRange = {};
- element.change = createChange();
- element.prefs = createDefaultDiffPrefs();
- element.reload(true);
- // Multiple cascading microtasks are scheduled.
- await flush();
- notifySyntaxProcessed();
- await waitUntil(() => element.reporting.timeEnd.callCount === 4);
- const calls = element.reporting.timeEnd.getCalls();
- assert.equal(calls.length, 4);
- assert.equal(calls[0].args[0], 'Diff Load Render');
- assert.equal(calls[1].args[0], 'Diff Content Render');
- assert.equal(calls[2].args[0], 'Diff Syntax Render');
- assert.equal(calls[3].args[0], 'Diff Total Render');
- assert.isTrue(element.reporting.diffViewContentDisplayed.called);
- });
-
- test('completes reload promise after syntax layer processing', async () => {
- let notifySyntaxProcessed;
- sinon.stub(element.syntaxLayer, 'process').returns(new Promise(
- resolve => {
- notifySyntaxProcessed = resolve;
- }));
- stubRestApi('getDiff').returns(
- Promise.resolve({content: []}));
- element.patchRange = {};
- element.change = createChange();
- let reloadComplete = false;
- element.prefs = createDefaultDiffPrefs();
- element.reload().then(() => {
- reloadComplete = true;
- });
- // Multiple cascading microtasks are scheduled.
- await flush();
- assert.isFalse(reloadComplete);
- notifySyntaxProcessed();
- await waitUntil(() => reloadComplete);
- assert.isTrue(reloadComplete);
- });
- });
-
- test('reload() cancels before network resolves', () => {
- const cancelStub = sinon.stub(element.$.diff, 'cancel');
-
- // Stub the network calls into requests that never resolve.
- sinon.stub(element, '_getDiff').callsFake(() => new Promise(() => {}));
- element.patchRange = {};
- element.change = createChange();
-
- // Needs to be set to something first for it to cancel.
- element.diff = {
- content: [{
- a: ['foo'],
- }],
- };
-
- element.reload();
- assert.isTrue(cancelStub.called);
- });
-
- test('reload() loads files weblinks', async () => {
- element.change = createChange();
- const weblinksStub = sinon.stub(GerritNav, '_generateWeblinks')
- .returns({name: 'stubb', url: '#s'});
- stubRestApi('getDiff').returns(Promise.resolve({
- content: [],
- }));
- element.projectName = 'test-project';
- element.path = 'test-path';
- element.commitRange = {baseCommit: 'test-base', commit: 'test-commit'};
- element.patchRange = {};
-
- await element.reload();
-
- assert.equal(weblinksStub.callCount, 3);
- assert.deepEqual(weblinksStub.firstCall.args[0], {
- commit: 'test-base',
- file: 'test-path',
- options: {
- weblinks: undefined,
- },
- repo: 'test-project',
- type: GerritNav.WeblinkType.EDIT});
- assert.deepEqual(element.editWeblinks, [{
- name: 'stubb', url: '#s',
- }]);
- assert.deepEqual(weblinksStub.secondCall.args[0], {
- commit: 'test-base',
- file: 'test-path',
- options: {
- weblinks: undefined,
- },
- repo: 'test-project',
- type: GerritNav.WeblinkType.FILE});
- assert.deepEqual(weblinksStub.thirdCall.args[0], {
- commit: 'test-commit',
- file: 'test-path',
- options: {
- weblinks: undefined,
- },
- repo: 'test-project',
- type: GerritNav.WeblinkType.FILE});
- assert.deepEqual(element.filesWeblinks, {
- meta_a: [{name: 'stubb', url: '#s'}],
- meta_b: [{name: 'stubb', url: '#s'}],
- });
- });
-
- test('prefetch getDiff', async () => {
- const diffRestApiStub = stubRestApi('getDiff')
- .returns(Promise.resolve({content: []}));
- element.changeNum = 123;
- element.patchRange = {basePatchNum: 1, patchNum: 2};
- element.path = 'file.txt';
- element.prefetchDiff();
- await element._getDiff();
- assert.isTrue(diffRestApiStub.calledOnce);
- });
-
- test('_getDiff handles null diff responses', async () => {
- stubRestApi('getDiff').returns(Promise.resolve(null));
- element.changeNum = 123;
- element.patchRange = {basePatchNum: 1, patchNum: 2};
- element.path = 'file.txt';
- await element._getDiff();
- });
-
- test('reload resolves on error', () => {
- const onErrStub = sinon.stub(element, '_handleGetDiffError');
- const error = new Response(null, {ok: false, status: 500});
- stubRestApi('getDiff').callsFake(
- (changeNum, basePatchNum, patchNum, path, whitespace, onErr) => {
- onErr(error);
- });
- element.patchRange = {};
- return element.reload().then(() => {
- assert.isTrue(onErrStub.calledOnce);
- });
- });
-
- suite('_handleGetDiffError', () => {
- let serverErrorStub;
- let pageErrorStub;
-
- setup(() => {
- serverErrorStub = sinon.stub();
- addListenerForTest(document, 'server-error', serverErrorStub);
- pageErrorStub = sinon.stub();
- addListenerForTest(document, 'page-error', pageErrorStub);
- });
-
- test('page error on HTTP-409', () => {
- element._handleGetDiffError({status: 409});
- assert.isTrue(serverErrorStub.calledOnce);
- assert.isFalse(pageErrorStub.called);
- assert.isNotOk(element._errorMessage);
- });
-
- test('server error on non-HTTP-409', () => {
- element._handleGetDiffError({
- status: 500,
- text: () => Promise.resolve(''),
- });
- assert.isFalse(serverErrorStub.called);
- assert.isTrue(pageErrorStub.calledOnce);
- assert.isNotOk(element._errorMessage);
- });
-
- test('error message if showLoadFailure', () => {
- element.showLoadFailure = true;
- element._handleGetDiffError({status: 500, statusText: 'Failure!'});
- assert.isFalse(serverErrorStub.called);
- assert.isFalse(pageErrorStub.called);
- assert.equal(element._errorMessage,
- 'Encountered error when loading the diff: 500 Failure!');
- });
- });
-
- suite('image diffs', () => {
- let mockFile1;
- let mockFile2;
- setup(() => {
- mockFile1 = {
- body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
- 'wsAAAAAAAAAAAAAAAAA/w==',
- type: 'image/bmp',
- };
- mockFile2 = {
- body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
- 'wsAAAAAAAAAAAAA/////w==',
- type: 'image/bmp',
- };
-
- element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
- element.change = createChange();
- element.comments = {
- left: [],
- right: [],
- meta: {patchRange: element.patchRange},
- };
- });
-
- test('renders image diffs with same file name', async () => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index 2adc47d..f9c2f2c 100644',
- '--- a/carrot.jpg',
- '+++ b/carrot.jpg',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
- stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
- stubRestApi('getImagesForDiff').returns(Promise.resolve({
- baseImage: {
- ...mockFile1,
- _expectedType: 'image/jpeg',
- _name: 'carrot.jpg',
- },
- revisionImage: {
- ...mockFile2,
- _expectedType: 'image/jpeg',
- _name: 'carrot.jpg',
- },
- }));
-
- element.prefs = createDefaultDiffPrefs();
- element.reload();
- await waitForEventOnce(element, 'render');
-
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
-
- // Left image rendered with the parent commit's version of the file.
- const leftImage =
- element.$.diff.$.diffTable.querySelector('td.left img');
- const leftLabel =
- element.$.diff.$.diffTable.querySelector('td.left label');
- const leftLabelContent = leftLabel.querySelector('.label');
- const leftLabelName = leftLabel.querySelector('.name');
-
- const rightImage =
- element.$.diff.$.diffTable.querySelector('td.right img');
- const rightLabel = element.$.diff.$.diffTable.querySelector(
- 'td.right label');
- const rightLabelContent = rightLabel.querySelector('.label');
- const rightLabelName = rightLabel.querySelector('.name');
-
- assert.isOk(leftImage);
- assert.equal(leftImage.getAttribute('src'),
- 'data:image/bmp;base64,' + mockFile1.body);
- assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
- assert.isNotOk(leftLabelName);
-
- assert.isOk(rightImage);
- assert.equal(rightImage.getAttribute('src'),
- 'data:image/bmp;base64,' + mockFile2.body);
- assert.equal(rightLabelContent.textContent, '1×1 image/bmp');
- assert.isNotOk(rightLabelName);
- });
-
- test('renders image diffs with a different file name', async () => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot2.jpg',
- 'index 2adc47d..f9c2f2c 100644',
- '--- a/carrot.jpg',
- '+++ b/carrot2.jpg',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
- stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
- stubRestApi('getImagesForDiff').returns(Promise.resolve({
- baseImage: {
- ...mockFile1,
- _expectedType: 'image/jpeg',
- _name: 'carrot.jpg',
- },
- revisionImage: {
- ...mockFile2,
- _expectedType: 'image/jpeg',
- _name: 'carrot2.jpg',
- },
- }));
-
- element.prefs = createDefaultDiffPrefs();
- element.reload();
- await waitForEventOnce(element, 'render');
-
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
-
- // Left image rendered with the parent commit's version of the file.
- const leftImage =
- element.$.diff.$.diffTable.querySelector('td.left img');
- const leftLabel =
- element.$.diff.$.diffTable.querySelector('td.left label');
- const leftLabelContent = leftLabel.querySelector('.label');
- const leftLabelName = leftLabel.querySelector('.name');
-
- const rightImage =
- element.$.diff.$.diffTable.querySelector('td.right img');
- const rightLabel = element.$.diff.$.diffTable.querySelector(
- 'td.right label');
- const rightLabelContent = rightLabel.querySelector('.label');
- const rightLabelName = rightLabel.querySelector('.name');
-
- assert.isOk(rightLabelName);
- assert.isOk(leftLabelName);
- assert.equal(leftLabelName.textContent, mockDiff.meta_a.name);
- assert.equal(rightLabelName.textContent, mockDiff.meta_b.name);
-
- assert.isOk(leftImage);
- assert.equal(leftImage.getAttribute('src'),
- 'data:image/bmp;base64,' + mockFile1.body);
- assert.equal(leftLabelContent.textContent, '1×1 image/bmp');
-
- assert.isOk(rightImage);
- assert.equal(rightImage.getAttribute('src'),
- 'data:image/bmp;base64,' + mockFile2.body);
- assert.equal(rightLabelContent.textContent, '1×1 image/bmp');
- });
-
- test('renders added image', async () => {
- const mockDiff = {
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'ADDED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index 0000000..f9c2f2c 100644',
- '--- /dev/null',
- '+++ b/carrot.jpg',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
- stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
- stubRestApi('getImagesForDiff').returns(Promise.resolve({
- baseImage: null,
- revisionImage: {
- ...mockFile2,
- _expectedType: 'image/jpeg',
- _name: 'carrot2.jpg',
- },
- }));
-
- const promise = mockPromise();
- element.addEventListener('render', () => {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
-
- const leftImage =
- element.$.diff.$.diffTable.querySelector('td.left img');
- const rightImage =
- element.$.diff.$.diffTable.querySelector('td.right img');
-
- assert.isNotOk(leftImage);
- assert.isOk(rightImage);
- promise.resolve();
- });
-
- element.prefs = createDefaultDiffPrefs();
- element.reload();
- await promise;
- });
-
- test('renders removed image', async () => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'DELETED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index f9c2f2c..0000000 100644',
- '--- a/carrot.jpg',
- '+++ /dev/null',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
- stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
- stubRestApi('getImagesForDiff').returns(Promise.resolve({
- baseImage: {
- ...mockFile1,
- _expectedType: 'image/jpeg',
- _name: 'carrot.jpg',
- },
- revisionImage: null,
- }));
-
- const promise = mockPromise();
- element.addEventListener('render', () => {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
-
- const leftImage =
- element.$.diff.$.diffTable.querySelector('td.left img');
- const rightImage =
- element.$.diff.$.diffTable.querySelector('td.right img');
-
- assert.isOk(leftImage);
- assert.isNotOk(rightImage);
- promise.resolve();
- });
-
- element.prefs = createDefaultDiffPrefs();
- element.reload();
- await promise;
- });
-
- test('does not render disallowed image type', async () => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil',
- lines: 560},
- intraline_status: 'OK',
- change_type: 'DELETED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index f9c2f2c..0000000 100644',
- '--- a/carrot.jpg',
- '+++ /dev/null',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
- mockFile1.type = 'image/jpeg-evil';
-
- stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
- stubRestApi('getImagesForDiff').returns(Promise.resolve({
- baseImage: {
- ...mockFile1,
- _expectedType: 'image/jpeg',
- _name: 'carrot.jpg',
- },
- revisionImage: null,
- }));
-
- const promise = mockPromise();
- element.addEventListener('render', () => {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage);
- const leftImage =
- element.$.diff.$.diffTable.querySelector('td.left img');
- assert.isNotOk(leftImage);
- promise.resolve();
- });
-
- element.prefs = createDefaultDiffPrefs();
- element.reload();
- await promise;
- });
- });
-
- test('cannot create comments when not logged in', () => {
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 2,
- };
- const showAuthRequireSpy = sinon.spy();
- element.addEventListener('show-auth-required', showAuthRequireSpy);
-
- element.dispatchEvent(new CustomEvent('create-comment', {
- detail: {
- lineNum: 3,
- side: Side.LEFT,
- path: '/p',
- },
- }));
-
- const threads = dom(element.$.diff)
- .queryDistributedElements('gr-comment-thread');
-
- assert.equal(threads.length, 0);
-
- assert.isTrue(showAuthRequireSpy.called);
- });
-
- test('delegates cancel()', () => {
- const stub = sinon.stub(element.$.diff, 'cancel');
- element.patchRange = {};
- element.cancel();
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 0);
- });
-
- test('delegates getCursorStops()', () => {
- const returnValue = [document.createElement('b')];
- const stub = sinon.stub(element.$.diff, 'getCursorStops')
- .returns(returnValue);
- assert.equal(element.getCursorStops(), returnValue);
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 0);
- });
-
- test('delegates isRangeSelected()', () => {
- const returnValue = true;
- const stub = sinon.stub(element.$.diff, 'isRangeSelected')
- .returns(returnValue);
- assert.equal(element.isRangeSelected(), returnValue);
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 0);
- });
-
- test('delegates toggleLeftDiff()', () => {
- const stub = sinon.stub(element.$.diff, 'toggleLeftDiff');
- element.toggleLeftDiff();
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 0);
- });
-
- suite('blame', () => {
- setup(async () => {
- element = basicFixture.instantiate();
- element.changeNum = 123;
- element.path = 'some/path';
- await flush();
- });
-
- test('clearBlame', () => {
- element._blame = [];
- const setBlameSpy = sinon.spy(element.$.diff.$.diffBuilder, 'setBlame');
- element.clearBlame();
- assert.isNull(element._blame);
- assert.isTrue(setBlameSpy.calledWithExactly(null));
- assert.equal(element.isBlameLoaded, false);
- });
-
- test('loadBlame', () => {
- const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
- const showAlertStub = sinon.stub();
- element.addEventListener('show-alert', showAlertStub);
- const getBlameStub = stubRestApi('getBlame')
- .returns(Promise.resolve(mockBlame));
- element.changeNum = 42;
- element.patchRange = {patchNum: 5, basePatchNum: 4};
- element.path = 'foo/bar.baz';
- return element.loadBlame().then(() => {
- assert.isTrue(getBlameStub.calledWithExactly(
- 42, 5, 'foo/bar.baz', true));
- assert.isFalse(showAlertStub.called);
- assert.equal(element._blame, mockBlame);
- assert.equal(element.isBlameLoaded, true);
- });
- });
-
- test('loadBlame empty', () => {
- const mockBlame = [];
- const showAlertStub = sinon.stub();
- element.addEventListener('show-alert', showAlertStub);
- stubRestApi('getBlame')
- .returns(Promise.resolve(mockBlame));
- element.changeNum = 42;
- element.patchRange = {patchNum: 5, basePatchNum: 4};
- element.path = 'foo/bar.baz';
- return element.loadBlame()
- .then(() => {
- assert.isTrue(false, 'Promise should not resolve');
- })
- .catch(() => {
- assert.isTrue(showAlertStub.calledOnce);
- assert.isNull(element._blame);
- assert.equal(element.isBlameLoaded, false);
- });
- });
- });
-
- test('getThreadEls() returns .comment-threads', () => {
- const threadEl = document.createElement('gr-comment-thread');
- threadEl.className = 'comment-thread';
- element.$.diff.appendChild(threadEl);
- assert.deepEqual(element.getThreadEls(), [threadEl]);
- });
-
- test('delegates addDraftAtLine(el)', () => {
- const param0 = document.createElement('b');
- const stub = sinon.stub(element.$.diff, 'addDraftAtLine');
- element.addDraftAtLine(param0);
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 1);
- assert.equal(stub.lastCall.args[0], param0);
- });
-
- test('delegates clearDiffContent()', () => {
- const stub = sinon.stub(element.$.diff, 'clearDiffContent');
- element.clearDiffContent();
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 0);
- });
-
- test('delegates toggleAllContext()', () => {
- const stub = sinon.stub(element.$.diff, 'toggleAllContext');
- element.toggleAllContext();
- assert.isTrue(stub.calledOnce);
- assert.equal(stub.lastCall.args.length, 0);
- });
-
- test('passes in noAutoRender', () => {
- const value = true;
- element.noAutoRender = value;
- assert.equal(element.$.diff.noAutoRender, value);
- });
-
- test('passes in path', () => {
- const value = 'some/file/path';
- element.path = value;
- assert.equal(element.$.diff.path, value);
- });
-
- test('passes in prefs', () => {
- const value = {};
- element.prefs = value;
- assert.equal(element.$.diff.prefs, value);
- });
-
- test('passes in displayLine', () => {
- const value = true;
- element.displayLine = value;
- assert.equal(element.$.diff.displayLine, value);
- });
-
- test('passes in hidden', () => {
- const value = true;
- element.hidden = value;
- assert.equal(element.$.diff.hidden, value);
- assert.isNotNull(element.getAttribute('hidden'));
- });
-
- test('passes in noRenderOnPrefsChange', () => {
- const value = true;
- element.noRenderOnPrefsChange = value;
- assert.equal(element.$.diff.noRenderOnPrefsChange, value);
- });
-
- test('passes in lineWrapping', () => {
- const value = true;
- element.lineWrapping = value;
- assert.equal(element.$.diff.lineWrapping, value);
- });
-
- test('passes in viewMode', () => {
- const value = 'SIDE_BY_SIDE';
- element.viewMode = value;
- assert.equal(element.$.diff.viewMode, value);
- });
-
- test('passes in lineOfInterest', () => {
- const value = {lineNum: 123, side: Side.LEFT};
- element.lineOfInterest = value;
- assert.equal(element.$.diff.lineOfInterest, value);
- });
-
- suite('_reportDiff', () => {
- let reportStub;
-
- setup(async () => {
- element = basicFixture.instantiate();
- element.changeNum = 123;
- element.path = 'file.txt';
- element.patchRange = {basePatchNum: 1};
- reportStub = sinon.stub(element.reporting, 'reportInteraction');
- await flush();
- });
-
- test('null and content-less', () => {
- element._reportDiff(null);
- assert.isFalse(reportStub.called);
-
- element._reportDiff({});
- assert.isFalse(reportStub.called);
- });
-
- test('diff w/ no delta', () => {
- const diff = {
- content: [
- {ab: ['foo', 'bar']},
- {ab: ['baz', 'foo']},
- ],
- };
- element._reportDiff(diff);
- assert.isTrue(reportStub.calledOnce);
- assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero');
- assert.isUndefined(reportStub.lastCall.args[1]);
- });
-
- test('diff w/ no rebase delta', () => {
- const diff = {
- content: [
- {ab: ['foo', 'bar']},
- {a: ['baz', 'foo']},
- {ab: ['foo', 'bar']},
- {a: ['baz', 'foo'], b: ['bar', 'baz']},
- {ab: ['foo', 'bar']},
- {b: ['baz', 'foo']},
- {ab: ['foo', 'bar']},
- ],
- };
- element._reportDiff(diff);
- assert.isTrue(reportStub.calledOnce);
- assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero');
- assert.isUndefined(reportStub.lastCall.args[1]);
- });
-
- test('diff w/ some rebase delta', () => {
- const diff = {
- content: [
- {ab: ['foo', 'bar']},
- {a: ['baz', 'foo'], due_to_rebase: true},
- {ab: ['foo', 'bar']},
- {a: ['baz', 'foo'], b: ['bar', 'baz']},
- {ab: ['foo', 'bar']},
- {b: ['baz', 'foo'], due_to_rebase: true},
- {ab: ['foo', 'bar']},
- {a: ['baz', 'foo']},
- ],
- };
- element._reportDiff(diff);
- assert.isTrue(reportStub.calledOnce);
- assert.isTrue(reportStub.calledWith(
- 'rebase-percent-nonzero',
- {percentRebaseDelta: 50}
- ));
- });
-
- test('diff w/ all rebase delta', () => {
- const diff = {content: [{
- a: ['foo', 'bar'],
- b: ['baz', 'foo'],
- due_to_rebase: true,
- }]};
- element._reportDiff(diff);
- assert.isTrue(reportStub.calledOnce);
- assert.isTrue(reportStub.calledWith(
- 'rebase-percent-nonzero',
- {percentRebaseDelta: 100}
- ));
- });
-
- test('diff against parent event', () => {
- element.patchRange.basePatchNum = 'PARENT';
- const diff = {content: [{
- a: ['foo', 'bar'],
- b: ['baz', 'foo'],
- }]};
- element._reportDiff(diff);
- assert.isTrue(reportStub.calledOnce);
- assert.equal(reportStub.lastCall.args[0], 'diff-against-parent');
- assert.isUndefined(reportStub.lastCall.args[1]);
- });
- });
-
- suite('createCheckEl method', () => {
- test('start_line:12', () => {
- const result = {
- codePointers: [{range: {start_line: 12}}],
- };
- const el = element.createCheckEl(result);
- assert.equal(el.getAttribute('slot'), 'right-12');
- assert.equal(el.getAttribute('diff-side'), 'right');
- assert.equal(el.getAttribute('line-num'), '12');
- assert.equal(el.getAttribute('range'), null);
- assert.equal(el.result, result);
- });
-
- test('start_line:13 end_line:14 without char positions', () => {
- const result = {
- codePointers: [{range: {start_line: 13, end_line: 14}}],
- };
- const el = element.createCheckEl(result);
- assert.equal(el.getAttribute('slot'), 'right-14');
- assert.equal(el.getAttribute('diff-side'), 'right');
- assert.equal(el.getAttribute('line-num'), '14');
- assert.equal(el.getAttribute('range'), null);
- assert.equal(el.result, result);
- });
-
- test('start_line:13 end_line:14 with char positions', () => {
- const result = {
- codePointers: [
- {
- range: {
- start_line: 13,
- end_line: 14,
- start_character: 5,
- end_character: 7,
- },
- },
- ],
- };
- const el = element.createCheckEl(result);
- assert.equal(el.getAttribute('slot'), 'right-14');
- assert.equal(el.getAttribute('diff-side'), 'right');
- assert.equal(el.getAttribute('line-num'), '14');
- assert.equal(el.getAttribute('range'),
- '{"start_line":13,' +
- '"end_line":14,' +
- '"start_character":5,' +
- '"end_character":7}');
- assert.equal(el.result, result);
- });
-
- test('empty range', () => {
- const result = {
- codePointers: [{range: {}}],
- };
- const el = element.createCheckEl(result);
- assert.equal(el.getAttribute('slot'), 'right-FILE');
- assert.equal(el.getAttribute('diff-side'), 'right');
- assert.equal(el.getAttribute('line-num'), 'FILE');
- assert.equal(el.getAttribute('range'), null);
- assert.equal(el.result, result);
- });
- });
-
- suite('create-comment', () => {
- setup(async () => {
- loggedIn = true;
- element.connectedCallback();
- await flush();
- });
-
- test('creates comments if they do not exist yet', () => {
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 2,
- };
-
- element.dispatchEvent(new CustomEvent('create-comment', {
- detail: {
- lineNum: 3,
- side: Side.LEFT,
- path: '/p',
- },
- }));
-
- let threads = dom(element.$.diff)
- .queryDistributedElements('gr-comment-thread');
-
- assert.equal(threads.length, 1);
- 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 = {
- start_line: 1,
- start_character: 1,
- end_line: 1,
- end_character: 3,
- };
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 3,
- };
-
- element.dispatchEvent(new CustomEvent('create-comment', {
- detail: {
- lineNum: 1,
- side: Side.LEFT,
- path: '/p',
- range,
- },
- }));
-
- threads = dom(element.$.diff)
- .queryDistributedElements('gr-comment-thread');
-
- assert.equal(threads.length, 2);
- 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', () => {
- element.patchRange = {
- basePatchNum: 2,
- patchNum: 3,
- };
-
- element.dispatchEvent(new CustomEvent('create-comment', {
- detail: {
- side: Side.RIGHT,
- },
- }));
-
- const threadEl = dom(element.$.diff)
- .queryDistributedElements('gr-comment-thread')[0];
-
- 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', () => {
- element.patchRange = {
- basePatchNum: 'PARENT',
- patchNum: 3,
- };
-
- element.dispatchEvent(new CustomEvent('create-comment', {
- detail: {
- side: Side.LEFT,
- },
- }));
-
- const threadEl = dom(element.$.diff)
- .queryDistributedElements('gr-comment-thread')[0];
-
- assert.equal(threadEl.thread.commentSide, 'PARENT');
- assert.equal(threadEl.getAttribute('diff-side'), Side.LEFT);
- });
-
- test('should be on parent if right and base negative', () => {
- element.patchRange = {
- basePatchNum: -2, // merge parents have negative numbers
- patchNum: 3,
- };
-
- element.dispatchEvent(new CustomEvent('create-comment', {
- detail: {
- side: Side.LEFT,
- },
- }));
-
- const threadEl = dom(element.$.diff)
- .queryDistributedElements('gr-comment-thread')[0];
-
- assert.equal(threadEl.thread.commentSide, 'PARENT');
- assert.equal(threadEl.getAttribute('diff-side'), Side.LEFT);
- });
-
- test('should not be on parent otherwise', () => {
- element.patchRange = {
- basePatchNum: 2, // merge parents have negative numbers
- patchNum: 3,
- };
-
- element.dispatchEvent(new CustomEvent('create-comment', {
- detail: {
- side: Side.LEFT,
- },
- }));
-
- const threadEl = dom(element.$.diff)
- .queryDistributedElements('gr-comment-thread')[0];
-
- 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', 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: Side.LEFT,
- path: '/p',
- },
- }));
-
- const threads = dom(element.$.diff)
- .queryDistributedElements('gr-comment-thread');
-
- assert.equal(threads.length, 1);
- 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', 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: Side.RIGHT,
- path: '/p',
- },
- }));
-
- const threads = dom(element.$.diff)
- .queryDistributedElements('gr-comment-thread');
-
- assert.equal(threads.length, 1);
- 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', async () => {
- element.patchRange = {
- basePatchNum: 2,
- patchNum: 3,
- };
- element.file = {basePath: 'file_renamed.txt', path: element.path};
- await flush();
-
- 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];
-
- let threads = dom(element.$.diff)
- .queryDistributedElements('gr-comment-thread');
-
- assert.equal(threads.length, 1);
- element.threads= [...element.threads, thread];
-
- threads = dom(element.$.diff)
- .queryDistributedElements('gr-comment-thread');
- // Threads have same rootId so element is reused
- assert.equal(threads.length, 1);
-
- const newThread = {...thread};
- newThread.rootId = 'differentRootId';
- element.threads= [...element.threads, newThread];
- threads = dom(element.$.diff)
- .queryDistributedElements('gr-comment-thread');
- // New thread has a different rootId
- assert.equal(threads.length, 2);
- });
-
- 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: Side.LEFT,
- path: '/p',
- },
- }));
-
- const threads =
- dom(element.$.diff).queryDistributedElements('gr-comment-thread');
-
- assert.equal(threads.length, 1);
- 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', () => {
- const alertSpy = sinon.spy();
- element.addEventListener('show-alert', alertSpy);
-
- const diffSide = Side.LEFT;
- element.patchRange = {
- basePatchNum: EditPatchSetNum,
- patchNum: 3,
- };
- element.dispatchEvent(new CustomEvent('create-comment', {
- detail: {
- side: diffSide,
- path: '/p',
- },
- }));
-
- const threads =
- dom(element.$.diff).queryDistributedElements('gr-comment-thread');
- assert.equal(threads.length, 0);
- assert.isTrue(alertSpy.called);
- });
-
- test('cannot create thread on an edit base', () => {
- const alertSpy = sinon.spy();
- element.addEventListener('show-alert', alertSpy);
-
- const diffSide = Side.LEFT;
- element.patchRange = {
- basePatchNum: ParentPatchSetNum,
- patchNum: EditPatchSetNum,
- };
- element.dispatchEvent(new CustomEvent('create-comment', {
- detail: {
- side: diffSide,
- path: '/p',
- },
- }));
-
- const threads = dom(element.$.diff)
- .queryDistributedElements('gr-comment-thread');
- assert.equal(threads.length, 0);
- assert.isTrue(alertSpy.called);
- });
- });
-
- test('_filterThreadElsForLocation with no threads', () => {
- const line = {beforeNumber: 3, afterNumber: 5};
-
- const threads = [];
- assert.deepEqual(element._filterThreadElsForLocation(threads, line), []);
- assert.deepEqual(element._filterThreadElsForLocation(threads, line,
- Side.LEFT), []);
- assert.deepEqual(element._filterThreadElsForLocation(threads, line,
- Side.RIGHT), []);
- });
-
- test('_filterThreadElsForLocation for line comments', () => {
- const line = {beforeNumber: 3, afterNumber: 5};
-
- const l3 = document.createElement('div');
- l3.setAttribute('line-num', 3);
- l3.setAttribute('diff-side', Side.LEFT);
-
- const l5 = document.createElement('div');
- l5.setAttribute('line-num', 5);
- l5.setAttribute('diff-side', Side.LEFT);
-
- const r3 = document.createElement('div');
- r3.setAttribute('line-num', 3);
- r3.setAttribute('diff-side', Side.RIGHT);
-
- const r5 = document.createElement('div');
- r5.setAttribute('line-num', 5);
- r5.setAttribute('diff-side', Side.RIGHT);
-
- const threadEls = [l3, l5, r3, r5];
- assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
- Side.LEFT), [l3]);
- assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
- Side.RIGHT), [r5]);
- });
-
- test('_filterThreadElsForLocation for file comments', () => {
- const line = {beforeNumber: 'FILE', afterNumber: 'FILE'};
-
- const l = document.createElement('div');
- l.setAttribute('diff-side', Side.LEFT);
- l.setAttribute('line-num', 'FILE');
-
- const r = document.createElement('div');
- r.setAttribute('diff-side', Side.RIGHT);
- r.setAttribute('line-num', 'FILE');
-
- const threadEls = [l, r];
- assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
- Side.LEFT), [l]);
- assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
- Side.RIGHT), [r]);
- });
-
- suite('syntax layer with syntax_highlighting on', () => {
- setup(() => {
- const prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- context: -1,
- syntax_highlighting: true,
- };
- element.patchRange = {};
- element.prefs = prefs;
- element.changeNum = 123;
- element.change = createChange();
- element.path = 'some/path';
- });
-
- test('gr-diff-host provides syntax highlighting layer', async () => {
- stubRestApi('getDiff').returns(Promise.resolve({content: []}));
- await element.reload();
- assert.equal(element.$.diff.layers[1], element.syntaxLayer);
- });
-
- test('rendering normal-sized diff does not disable syntax', () => {
- element.diff = {
- content: [{
- a: ['foo'],
- }],
- };
- assert.isTrue(element.syntaxLayer.enabled);
- });
-
- test('rendering large diff disables syntax', () => {
- // Before it renders, set the first diff line to 500 '*' characters.
- element.diff = {
- content: [{
- a: [new Array(501).join('*')],
- }],
- };
- assert.isFalse(element.syntaxLayer.enabled);
- });
-
- test('starts syntax layer processing on render event', async () => {
- sinon.stub(element.syntaxLayer, 'process')
- .returns(Promise.resolve());
- stubRestApi('getDiff').returns(Promise.resolve({content: []}));
- await element.reload();
- element.dispatchEvent(
- new CustomEvent('render', {bubbles: true, composed: true}));
- assert.isTrue(element.syntaxLayer.process.called);
- });
- });
-
- suite('syntax layer with syntax_highlighting off', () => {
- setup(() => {
- const prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- context: -1,
- };
- element.diff = {
- content: [{
- a: ['foo'],
- }],
- };
- element.patchRange = {};
- element.change = createChange();
- element.prefs = prefs;
- });
-
- test('gr-diff-host provides syntax highlighting layer', async () => {
- stubRestApi('getDiff').returns(Promise.resolve({content: []}));
- await element.reload();
- assert.equal(element.$.diff.layers[1], element.syntaxLayer);
- });
-
- test('syntax layer should be disabled', () => {
- assert.isFalse(element.syntaxLayer.enabled);
- });
-
- test('still disabled for large diff', () => {
- // Before it renders, set the first diff line to 500 '*' characters.
- element.diff = {
- content: [{
- a: [new Array(501).join('*')],
- }],
- };
- assert.isFalse(element.syntaxLayer.enabled);
- });
- });
-
- suite('coverage layer', () => {
- let notifyStub;
- let coverageProviderStub;
- const exampleRanges = [
- {
- type: CoverageType.COVERED,
- side: Side.RIGHT,
- code_range: {
- start_line: 1,
- end_line: 2,
- },
- },
- {
- type: CoverageType.NOT_COVERED,
- side: Side.RIGHT,
- code_range: {
- start_line: 3,
- end_line: 4,
- },
- },
- ];
-
- setup(async () => {
- notifyStub = sinon.stub();
- coverageProviderStub = sinon.stub().returns(
- Promise.resolve(exampleRanges));
-
- element = basicFixture.instantiate();
- sinon.stub(element.jsAPI, 'getCoverageAnnotationApis').returns(
- Promise.resolve([{
- notify: notifyStub,
- getCoverageProvider() {
- return coverageProviderStub;
- },
- }]));
- element.changeNum = 123;
- element.change = createChange();
- element.path = 'some/path';
- const prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- context: -1,
- };
- element.diff = {
- content: [{
- a: ['foo'],
- }],
- };
- element.patchRange = {};
- element.prefs = prefs;
- stubRestApi('getDiff').returns(Promise.resolve(element.diff));
- await flush();
- });
-
- test('getCoverageAnnotationApis should be called', async () => {
- await element.reload();
- assert.isTrue(element.jsAPI.getCoverageAnnotationApis.calledOnce);
- });
-
- test('coverageRangeChanged should be called', async () => {
- await element.reload();
- assert.equal(notifyStub.callCount, 2);
- assert.isTrue(notifyStub.calledWithExactly(
- 'some/path', 1, 2, Side.RIGHT));
- assert.isTrue(notifyStub.calledWithExactly(
- 'some/path', 3, 4, Side.RIGHT));
- });
-
- test('provider is called with appropriate params', async () => {
- element.patchRange.basePatchNum = 1;
- element.patchRange.patchNum = 3;
-
- await element.reload();
- assert.isTrue(coverageProviderStub.calledWithExactly(
- 123, 'some/path', 1, 3, element.change));
- });
-
- test('provider is called with appropriate params - special patchset values',
- async () => {
- element.patchRange.basePatchNum = 'PARENT';
- element.patchRange.patchNum = 'invalid';
-
- await element.reload();
- assert.isTrue(coverageProviderStub.calledWithExactly(
- 123, 'some/path', undefined, undefined, element.change));
- });
- });
-
- suite('trailing newlines', () => {
- setup(() => {
- });
-
- suite('_lastChunkForSide', () => {
- test('deltas', () => {
- const diff = {content: [
- {a: ['foo', 'bar'], b: ['baz']},
- {ab: ['foo', 'bar', 'baz']},
- {b: ['foo']},
- ]};
- assert.equal(element._lastChunkForSide(diff, false), diff.content[2]);
- assert.equal(element._lastChunkForSide(diff, true), diff.content[1]);
-
- diff.content.push({a: ['foo'], b: ['bar']});
- assert.equal(element._lastChunkForSide(diff, false), diff.content[3]);
- assert.equal(element._lastChunkForSide(diff, true), diff.content[3]);
- });
-
- test('addition with a undefined', () => {
- const diff = {content: [
- {b: ['foo', 'bar', 'baz']},
- ]};
- assert.equal(element._lastChunkForSide(diff, false), diff.content[0]);
- assert.isNull(element._lastChunkForSide(diff, true));
- });
-
- test('addition with a empty', () => {
- const diff = {content: [
- {a: [], b: ['foo', 'bar', 'baz']},
- ]};
- assert.equal(element._lastChunkForSide(diff, false), diff.content[0]);
- assert.isNull(element._lastChunkForSide(diff, true));
- });
-
- test('deletion with b undefined', () => {
- const diff = {content: [
- {a: ['foo', 'bar', 'baz']},
- ]};
- assert.isNull(element._lastChunkForSide(diff, false));
- assert.equal(element._lastChunkForSide(diff, true), diff.content[0]);
- });
-
- test('deletion with b empty', () => {
- const diff = {content: [
- {a: ['foo', 'bar', 'baz'], b: []},
- ]};
- assert.isNull(element._lastChunkForSide(diff, false));
- assert.equal(element._lastChunkForSide(diff, true), diff.content[0]);
- });
-
- test('empty', () => {
- const diff = {content: []};
- assert.isNull(element._lastChunkForSide(diff, false));
- assert.isNull(element._lastChunkForSide(diff, true));
- });
- });
-
- suite('_hasTrailingNewlines', () => {
- test('shared no trailing', () => {
- const diff = undefined;
- sinon.stub(element, '_lastChunkForSide')
- .returns({ab: ['foo', 'bar']});
- assert.isFalse(element._hasTrailingNewlines(diff, false));
- assert.isFalse(element._hasTrailingNewlines(diff, true));
- });
-
- test('delta trailing in right', () => {
- const diff = undefined;
- sinon.stub(element, '_lastChunkForSide')
- .returns({a: ['foo', 'bar'], b: ['baz', '']});
- assert.isTrue(element._hasTrailingNewlines(diff, false));
- assert.isFalse(element._hasTrailingNewlines(diff, true));
- });
-
- test('addition', () => {
- const diff = undefined;
- sinon.stub(element, '_lastChunkForSide').callsFake((diff, leftSide) => {
- if (leftSide) { return null; }
- return {b: ['foo', '']};
- });
- assert.isTrue(element._hasTrailingNewlines(diff, false));
- assert.isNull(element._hasTrailingNewlines(diff, true));
- });
-
- test('deletion', () => {
- const diff = undefined;
- sinon.stub(element, '_lastChunkForSide').callsFake((diff, leftSide) => {
- if (!leftSide) { return null; }
- return {a: ['foo']};
- });
- assert.isNull(element._hasTrailingNewlines(diff, false));
- assert.isFalse(element._hasTrailingNewlines(diff, true));
- });
- });
- });
-});
-
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
new file mode 100644
index 0000000..446d569
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
@@ -0,0 +1,1725 @@
+/**
+ * @license
+ * Copyright 2018 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import './gr-diff-host';
+import {
+ CommentSide,
+ createDefaultDiffPrefs,
+ Side,
+} from '../../../constants/constants';
+import {
+ createBlame,
+ createChange,
+ createComment,
+ createCommentThread,
+ createDiff,
+ createPatchRange,
+ createRunResult,
+} from '../../../test/test-data-generators';
+import {
+ addListenerForTest,
+ mockPromise,
+ query,
+ queryAll,
+ queryAndAssert,
+ stubReporting,
+ stubRestApi,
+ waitUntil,
+} from '../../../test/test-utils';
+import {
+ BasePatchSetNum,
+ BlameInfo,
+ CommentRange,
+ CommitId,
+ EditPatchSetNum,
+ ImageInfo,
+ NumericChangeId,
+ ParentPatchSetNum,
+ PatchSetNum,
+ RepoName,
+ RevisionPatchSetNum,
+ UrlEncodedCommentId,
+} from '../../../types/common';
+import {CoverageType} from '../../../types/types';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {GrDiffBuilderImage} from '../../../embed/diff/gr-diff-builder/gr-diff-builder-image';
+import {waitForEventOnce} from '../../../utils/event-util';
+import {GrDiffHost, LineInfo} from './gr-diff-host';
+import {DiffInfo, DiffViewMode, IgnoreWhitespaceType} from '../../../api/diff';
+import {ErrorCallback} from '../../../api/rest';
+import {SinonStub} from 'sinon';
+import {RunResult} from '../../../models/checks/checks-model';
+import {GrCommentThread} from '../../shared/gr-comment-thread/gr-comment-thread';
+import {assertIsDefined} from '../../../utils/common-util';
+import {GrAnnotationActionsInterface} from '../../shared/gr-js-api-interface/gr-annotation-actions-js-api';
+
+const basicFixture = fixtureFromElement('gr-diff-host');
+
+suite('gr-diff-host tests', () => {
+ let element: GrDiffHost;
+ let loggedIn: boolean;
+
+ setup(async () => {
+ loggedIn = false;
+ stubRestApi('getLoggedIn').callsFake(() => Promise.resolve(loggedIn));
+ element = basicFixture.instantiate();
+ element.changeNum = 123 as NumericChangeId;
+ element.path = 'some/path';
+ await flush();
+ });
+
+ suite('plugin layers', () => {
+ let getDiffLayersStub: sinon.SinonStub;
+ const pluginLayers = [{annotate: () => {}}, {annotate: () => {}}];
+ setup(() => {
+ element = basicFixture.instantiate();
+ getDiffLayersStub = sinon
+ .stub(element.jsAPI, 'getDiffLayers')
+ .returns(pluginLayers);
+ element.changeNum = 123 as NumericChangeId;
+ element.path = 'some/path';
+ });
+ test('plugin layers requested', async () => {
+ element.change = createChange();
+ stubRestApi('getDiff').returns(Promise.resolve(createDiff()));
+ await element.reload();
+ assert(getDiffLayersStub.called);
+ });
+ });
+
+ suite('render reporting', () => {
+ test('ends total and syntax timer after syntax layer', async () => {
+ const displayedStub = stubReporting('diffViewContentDisplayed');
+ const timeEndStub = sinon.stub(element.reporting, 'timeEnd');
+ let notifySyntaxProcessed: () => void = () => {};
+ sinon.stub(element.syntaxLayer, 'process').returns(
+ new Promise(resolve => {
+ notifySyntaxProcessed = resolve;
+ })
+ );
+ stubRestApi('getDiff').returns(Promise.resolve(createDiff()));
+ element.patchRange = createPatchRange();
+ element.change = createChange();
+ element.prefs = createDefaultDiffPrefs();
+ element.reload(true);
+ // Multiple cascading microtasks are scheduled.
+ await flush();
+ notifySyntaxProcessed();
+ await waitUntil(() => timeEndStub.callCount === 4);
+ const calls = timeEndStub.getCalls();
+ assert.equal(calls.length, 4);
+ assert.equal(calls[0].args[0], 'Diff Load Render');
+ assert.equal(calls[1].args[0], 'Diff Content Render');
+ assert.equal(calls[2].args[0], 'Diff Syntax Render');
+ assert.equal(calls[3].args[0], 'Diff Total Render');
+ assert.isTrue(displayedStub.called);
+ });
+
+ test('completes reload promise after syntax layer processing', async () => {
+ let notifySyntaxProcessed: () => void = () => {};
+ sinon.stub(element.syntaxLayer, 'process').returns(
+ new Promise(resolve => {
+ notifySyntaxProcessed = resolve;
+ })
+ );
+ stubRestApi('getDiff').returns(Promise.resolve(createDiff()));
+ element.patchRange = createPatchRange();
+ element.change = createChange();
+ let reloadComplete = false;
+ element.prefs = createDefaultDiffPrefs();
+ element.reload().then(() => {
+ reloadComplete = true;
+ });
+ // Multiple cascading microtasks are scheduled.
+ await flush();
+ assert.isFalse(reloadComplete);
+ notifySyntaxProcessed();
+ await waitUntil(() => reloadComplete);
+ assert.isTrue(reloadComplete);
+ });
+ });
+
+ test('reload() cancels before network resolves', () => {
+ const cancelStub = sinon.stub(element.$.diff, 'cancel');
+
+ // Stub the network calls into requests that never resolve.
+ sinon.stub(element, '_getDiff').callsFake(() => new Promise(() => {}));
+ element.patchRange = createPatchRange();
+ element.change = createChange();
+
+ // Needs to be set to something first for it to cancel.
+ element.diff = createDiff();
+
+ element.reload();
+ assert.isTrue(cancelStub.called);
+ });
+
+ test('reload() loads files weblinks', async () => {
+ element.change = createChange();
+ const weblinksStub = sinon
+ .stub(GerritNav, '_generateWeblinks')
+ .returns({name: 'stubb', url: '#s'});
+ stubRestApi('getDiff').returns(Promise.resolve(createDiff()));
+ element.projectName = 'test-project' as RepoName;
+ element.path = 'test-path';
+ element.commitRange = {
+ baseCommit: 'test-base' as CommitId,
+ commit: 'test-commit' as CommitId,
+ };
+ element.patchRange = createPatchRange();
+
+ await element.reload();
+
+ assert.equal(weblinksStub.callCount, 3);
+ assert.deepEqual(weblinksStub.firstCall.args[0], {
+ commit: 'test-base' as CommitId,
+ file: 'test-path',
+ options: {
+ weblinks: undefined,
+ },
+ repo: 'test-project' as RepoName,
+ type: GerritNav.WeblinkType.EDIT,
+ });
+ assert.deepEqual(element.editWeblinks, [
+ {
+ name: 'stubb',
+ url: '#s',
+ },
+ ]);
+ assert.deepEqual(weblinksStub.secondCall.args[0], {
+ commit: 'test-base' as CommitId,
+ file: 'test-path',
+ options: {
+ weblinks: undefined,
+ },
+ repo: 'test-project' as RepoName,
+ type: GerritNav.WeblinkType.FILE,
+ });
+ assert.deepEqual(weblinksStub.thirdCall.args[0], {
+ commit: 'test-commit' as CommitId,
+ file: 'test-path',
+ options: {
+ weblinks: undefined,
+ },
+ repo: 'test-project' as RepoName,
+ type: GerritNav.WeblinkType.FILE,
+ });
+ assert.deepEqual(element.filesWeblinks, {
+ meta_a: [{name: 'stubb', url: '#s'}],
+ meta_b: [{name: 'stubb', url: '#s'}],
+ });
+ });
+
+ test('prefetch getDiff', async () => {
+ const diffRestApiStub = stubRestApi('getDiff').returns(
+ Promise.resolve(createDiff())
+ );
+ element.changeNum = 123 as NumericChangeId;
+ element.patchRange = createPatchRange();
+ element.path = 'file.txt';
+ element.prefetchDiff();
+ await element._getDiff();
+ assert.isTrue(diffRestApiStub.calledOnce);
+ });
+
+ test('_getDiff handles undefined diff responses', async () => {
+ stubRestApi('getDiff').returns(Promise.resolve(undefined));
+ element.changeNum = 123 as NumericChangeId;
+ element.patchRange = createPatchRange();
+ element.path = 'file.txt';
+ await element._getDiff();
+ });
+
+ test('reload resolves on error', () => {
+ const onErrStub = sinon.stub(element, '_handleGetDiffError');
+ const error = new Response(null, {status: 500});
+ stubRestApi('getDiff').callsFake(
+ (
+ _1: NumericChangeId,
+ _2: PatchSetNum,
+ _3: PatchSetNum,
+ _4: string,
+ _5?: IgnoreWhitespaceType,
+ onErr?: ErrorCallback
+ ) => {
+ if (onErr) onErr(error);
+ return Promise.resolve(undefined);
+ }
+ );
+ element.patchRange = createPatchRange();
+ return element.reload().then(() => {
+ assert.isTrue(onErrStub.calledOnce);
+ });
+ });
+
+ suite('_handleGetDiffError', () => {
+ let serverErrorStub: sinon.SinonStub;
+ let pageErrorStub: sinon.SinonStub;
+
+ setup(() => {
+ serverErrorStub = sinon.stub();
+ addListenerForTest(document, 'server-error', serverErrorStub);
+ pageErrorStub = sinon.stub();
+ addListenerForTest(document, 'page-error', pageErrorStub);
+ });
+
+ test('page error on HTTP-409', () => {
+ element._handleGetDiffError({status: 409} as Response);
+ assert.isTrue(serverErrorStub.calledOnce);
+ assert.isFalse(pageErrorStub.called);
+ assert.isNotOk(element._errorMessage);
+ });
+
+ test('server error on non-HTTP-409', () => {
+ element._handleGetDiffError({
+ status: 500,
+ text: () => Promise.resolve(''),
+ } as Response);
+ assert.isFalse(serverErrorStub.called);
+ assert.isTrue(pageErrorStub.calledOnce);
+ assert.isNotOk(element._errorMessage);
+ });
+
+ test('error message if showLoadFailure', () => {
+ element.showLoadFailure = true;
+ element._handleGetDiffError({
+ status: 500,
+ statusText: 'Failure!',
+ } as Response);
+ assert.isFalse(serverErrorStub.called);
+ assert.isFalse(pageErrorStub.called);
+ assert.equal(
+ element._errorMessage,
+ 'Encountered error when loading the diff: 500 Failure!'
+ );
+ });
+ });
+
+ suite('image diffs', () => {
+ let mockFile1: ImageInfo;
+ let mockFile2: ImageInfo;
+ setup(() => {
+ mockFile1 = {
+ body:
+ 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
+ 'wsAAAAAAAAAAAAAAAAA/w==',
+ type: 'image/bmp',
+ };
+ mockFile2 = {
+ body:
+ 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
+ 'wsAAAAAAAAAAAAA/////w==',
+ type: 'image/bmp',
+ };
+
+ element.patchRange = createPatchRange();
+ element.change = createChange();
+ });
+
+ test('renders image diffs with same file name', async () => {
+ const mockDiff: DiffInfo = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot.jpg',
+ 'index 2adc47d..f9c2f2c 100644',
+ '--- a/carrot.jpg',
+ '+++ b/carrot.jpg',
+ 'Binary files differ',
+ ],
+ content: [{skip: 66}],
+ binary: true,
+ };
+ stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
+ stubRestApi('getImagesForDiff').returns(
+ Promise.resolve({
+ baseImage: {
+ ...mockFile1,
+ _expectedType: 'image/jpeg',
+ _name: 'carrot.jpg',
+ },
+ revisionImage: {
+ ...mockFile2,
+ _expectedType: 'image/jpeg',
+ _name: 'carrot.jpg',
+ },
+ })
+ );
+
+ element.prefs = createDefaultDiffPrefs();
+ element.reload();
+ await waitForEventOnce(element, 'render');
+
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(element.$.diff.diffBuilder.builder, GrDiffBuilderImage);
+
+ // Left image rendered with the parent commit's version of the file.
+ const diffTable = element.$.diff.$.diffTable;
+ const leftImage = queryAndAssert(diffTable, 'td.left img');
+ const leftLabel = queryAndAssert(diffTable, 'td.left label');
+ const leftLabelContent = leftLabel.querySelector('.label');
+ const leftLabelName = leftLabel.querySelector('.name');
+
+ const rightImage = queryAndAssert(diffTable, 'td.right img');
+ const rightLabel = queryAndAssert(diffTable, 'td.right label');
+ const rightLabelContent = rightLabel.querySelector('.label');
+ const rightLabelName = rightLabel.querySelector('.name');
+
+ assert.isOk(leftImage);
+ assert.equal(
+ leftImage.getAttribute('src'),
+ 'data:image/bmp;base64,' + mockFile1.body
+ );
+ assert.isTrue(leftLabelContent?.textContent?.includes('image/bmp'));
+ assert.isNotOk(leftLabelName);
+
+ assert.isOk(rightImage);
+ assert.equal(
+ rightImage.getAttribute('src'),
+ 'data:image/bmp;base64,' + mockFile2.body
+ );
+ assert.isTrue(rightLabelContent?.textContent?.includes('image/bmp'));
+ assert.isNotOk(rightLabelName);
+ });
+
+ test('renders image diffs with a different file name', async () => {
+ const mockDiff: DiffInfo = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+ meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg', lines: 560},
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot2.jpg',
+ 'index 2adc47d..f9c2f2c 100644',
+ '--- a/carrot.jpg',
+ '+++ b/carrot2.jpg',
+ 'Binary files differ',
+ ],
+ content: [{skip: 66}],
+ binary: true,
+ };
+ stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
+ stubRestApi('getImagesForDiff').returns(
+ Promise.resolve({
+ baseImage: {
+ ...mockFile1,
+ _expectedType: 'image/jpeg',
+ _name: 'carrot.jpg',
+ },
+ revisionImage: {
+ ...mockFile2,
+ _expectedType: 'image/jpeg',
+ _name: 'carrot2.jpg',
+ },
+ })
+ );
+
+ element.prefs = createDefaultDiffPrefs();
+ element.reload();
+ await waitForEventOnce(element, 'render');
+
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(element.$.diff.diffBuilder.builder, GrDiffBuilderImage);
+
+ // Left image rendered with the parent commit's version of the file.
+ const diffTable = element.$.diff.$.diffTable;
+ const leftImage = queryAndAssert(diffTable, 'td.left img');
+ const leftLabel = queryAndAssert(diffTable, 'td.left label');
+ const leftLabelContent = leftLabel.querySelector('.label');
+ const leftLabelName = leftLabel.querySelector('.name');
+
+ const rightImage = queryAndAssert(diffTable, 'td.right img');
+ const rightLabel = queryAndAssert(diffTable, 'td.right label');
+ const rightLabelContent = rightLabel.querySelector('.label');
+ const rightLabelName = rightLabel.querySelector('.name');
+
+ assert.isOk(rightLabelName);
+ assert.isOk(leftLabelName);
+ assert.equal(leftLabelName?.textContent, mockDiff.meta_a?.name);
+ assert.equal(rightLabelName?.textContent, mockDiff.meta_b?.name);
+
+ assert.isOk(leftImage);
+ assert.equal(
+ leftImage.getAttribute('src'),
+ 'data:image/bmp;base64,' + mockFile1.body
+ );
+ assert.isTrue(leftLabelContent?.textContent?.includes('image/bmp'));
+
+ assert.isOk(rightImage);
+ assert.equal(
+ rightImage.getAttribute('src'),
+ 'data:image/bmp;base64,' + mockFile2.body
+ );
+ assert.isTrue(rightLabelContent?.textContent?.includes('image/bmp'));
+ });
+
+ test('renders added image', async () => {
+ const mockDiff: DiffInfo = {
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
+ intraline_status: 'OK',
+ change_type: 'ADDED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot.jpg',
+ 'index 0000000..f9c2f2c 100644',
+ '--- /dev/null',
+ '+++ b/carrot.jpg',
+ 'Binary files differ',
+ ],
+ content: [{skip: 66}],
+ binary: true,
+ };
+ stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
+ stubRestApi('getImagesForDiff').returns(
+ Promise.resolve({
+ baseImage: null,
+ revisionImage: {
+ ...mockFile2,
+ _expectedType: 'image/jpeg',
+ _name: 'carrot2.jpg',
+ },
+ })
+ );
+
+ const promise = mockPromise();
+ element.addEventListener('render', () => {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diff.diffBuilder.builder,
+ GrDiffBuilderImage
+ );
+
+ const diffTable = element.$.diff.$.diffTable;
+
+ const leftImage = query(diffTable, 'td.left img');
+ const rightImage = queryAndAssert(diffTable, 'td.right img');
+
+ assert.isNotOk(leftImage);
+ assert.isOk(rightImage);
+ promise.resolve();
+ });
+
+ element.prefs = createDefaultDiffPrefs();
+ element.reload();
+ await promise;
+ });
+
+ test('renders removed image', async () => {
+ const mockDiff: DiffInfo = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
+ intraline_status: 'OK',
+ change_type: 'DELETED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot.jpg',
+ 'index f9c2f2c..0000000 100644',
+ '--- a/carrot.jpg',
+ '+++ /dev/null',
+ 'Binary files differ',
+ ],
+ content: [{skip: 66}],
+ binary: true,
+ };
+ stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
+ stubRestApi('getImagesForDiff').returns(
+ Promise.resolve({
+ baseImage: {
+ ...mockFile1,
+ _expectedType: 'image/jpeg',
+ _name: 'carrot.jpg',
+ },
+ revisionImage: null,
+ })
+ );
+
+ const promise = mockPromise();
+ element.addEventListener('render', () => {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diff.diffBuilder.builder,
+ GrDiffBuilderImage
+ );
+
+ const diffTable = element.$.diff.$.diffTable;
+
+ const leftImage = queryAndAssert(diffTable, 'td.left img');
+ const rightImage = query(diffTable, 'td.right img');
+
+ assert.isOk(leftImage);
+ assert.isNotOk(rightImage);
+ promise.resolve();
+ });
+
+ element.prefs = createDefaultDiffPrefs();
+ element.reload();
+ await promise;
+ });
+
+ test('does not render disallowed image type', async () => {
+ const mockDiff: DiffInfo = {
+ meta_a: {
+ name: 'carrot.jpg',
+ content_type: 'image/jpeg-evil',
+ lines: 560,
+ },
+ intraline_status: 'OK',
+ change_type: 'DELETED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot.jpg',
+ 'index f9c2f2c..0000000 100644',
+ '--- a/carrot.jpg',
+ '+++ /dev/null',
+ 'Binary files differ',
+ ],
+ content: [{skip: 66}],
+ binary: true,
+ };
+ mockFile1.type = 'image/jpeg-evil';
+
+ stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
+ stubRestApi('getImagesForDiff').returns(
+ Promise.resolve({
+ baseImage: {
+ ...mockFile1,
+ _expectedType: 'image/jpeg',
+ _name: 'carrot.jpg',
+ },
+ revisionImage: null,
+ })
+ );
+
+ const promise = mockPromise();
+ element.addEventListener('render', () => {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diff.diffBuilder.builder,
+ GrDiffBuilderImage
+ );
+ const diffTable = element.$.diff.$.diffTable;
+
+ const leftImage = query(diffTable, 'td.left img');
+ assert.isNotOk(leftImage);
+ promise.resolve();
+ });
+
+ element.prefs = createDefaultDiffPrefs();
+ element.reload();
+ await promise;
+ });
+ });
+
+ test('cannot create comments when not logged in', () => {
+ element.patchRange = createPatchRange();
+ const showAuthRequireSpy = sinon.spy();
+ element.addEventListener('show-auth-required', showAuthRequireSpy);
+
+ element.dispatchEvent(
+ new CustomEvent('create-comment', {
+ detail: {
+ lineNum: 3,
+ side: Side.LEFT,
+ path: '/p',
+ },
+ })
+ );
+
+ const threads = queryAll(element.$.diff, 'gr-comment-thread');
+ assert.equal(threads.length, 0);
+ assert.isTrue(showAuthRequireSpy.called);
+ });
+
+ test('delegates cancel()', () => {
+ const stub = sinon.stub(element.$.diff, 'cancel');
+ element.patchRange = createPatchRange();
+ element.cancel();
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 0);
+ });
+
+ test('delegates getCursorStops()', () => {
+ const returnValue = [document.createElement('b')];
+ const stub = sinon
+ .stub(element.$.diff, 'getCursorStops')
+ .returns(returnValue);
+ assert.equal(element.getCursorStops(), returnValue);
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 0);
+ });
+
+ test('delegates isRangeSelected()', () => {
+ const returnValue = true;
+ const stub = sinon
+ .stub(element.$.diff, 'isRangeSelected')
+ .returns(returnValue);
+ assert.equal(element.isRangeSelected(), returnValue);
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 0);
+ });
+
+ test('delegates toggleLeftDiff()', () => {
+ const stub = sinon.stub(element.$.diff, 'toggleLeftDiff');
+ element.toggleLeftDiff();
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 0);
+ });
+
+ suite('blame', () => {
+ setup(async () => {
+ element = basicFixture.instantiate();
+ element.changeNum = 123 as NumericChangeId;
+ element.path = 'some/path';
+ await flush();
+ });
+
+ test('clearBlame', () => {
+ element._blame = [];
+ const setBlameSpy = sinon.spy(element.$.diff.diffBuilder, 'setBlame');
+ element.clearBlame();
+ assert.isNull(element._blame);
+ assert.isTrue(setBlameSpy.calledWithExactly(null));
+ assert.equal(element.isBlameLoaded, false);
+ });
+
+ test('loadBlame', () => {
+ const mockBlame: BlameInfo[] = [createBlame()];
+ const showAlertStub = sinon.stub();
+ element.addEventListener('show-alert', showAlertStub);
+ const getBlameStub = stubRestApi('getBlame').returns(
+ Promise.resolve(mockBlame)
+ );
+ const changeNum = 42 as NumericChangeId;
+ element.changeNum = changeNum;
+ element.patchRange = createPatchRange();
+ element.path = 'foo/bar.baz';
+ return element.loadBlame().then(() => {
+ assert.isTrue(
+ getBlameStub.calledWithExactly(
+ changeNum,
+ 1 as RevisionPatchSetNum,
+ 'foo/bar.baz',
+ true
+ )
+ );
+ assert.isFalse(showAlertStub.called);
+ assert.equal(element._blame, mockBlame);
+ assert.equal(element.isBlameLoaded, true);
+ });
+ });
+
+ test('loadBlame empty', () => {
+ const mockBlame: BlameInfo[] = [];
+ const showAlertStub = sinon.stub();
+ element.addEventListener('show-alert', showAlertStub);
+ stubRestApi('getBlame').returns(Promise.resolve(mockBlame));
+ const changeNum = 42 as NumericChangeId;
+ element.changeNum = changeNum;
+ element.patchRange = createPatchRange();
+ element.path = 'foo/bar.baz';
+ return element
+ .loadBlame()
+ .then(() => {
+ assert.isTrue(false, 'Promise should not resolve');
+ })
+ .catch(() => {
+ assert.isTrue(showAlertStub.calledOnce);
+ assert.isNull(element._blame);
+ assert.equal(element.isBlameLoaded, false);
+ });
+ });
+ });
+
+ test('getThreadEls() returns .comment-threads', () => {
+ const threadEl = document.createElement('gr-comment-thread');
+ threadEl.className = 'comment-thread';
+ element.$.diff.appendChild(threadEl);
+ assert.deepEqual(element.getThreadEls(), [threadEl]);
+ });
+
+ test('delegates addDraftAtLine(el)', () => {
+ const param0 = document.createElement('b');
+ const stub = sinon.stub(element.$.diff, 'addDraftAtLine');
+ element.addDraftAtLine(param0);
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 1);
+ assert.equal(stub.lastCall.args[0], param0);
+ });
+
+ test('delegates clearDiffContent()', () => {
+ const stub = sinon.stub(element.$.diff, 'clearDiffContent');
+ element.clearDiffContent();
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 0);
+ });
+
+ test('delegates toggleAllContext()', () => {
+ const stub = sinon.stub(element.$.diff, 'toggleAllContext');
+ element.toggleAllContext();
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args.length, 0);
+ });
+
+ test('passes in noAutoRender', () => {
+ const value = true;
+ element.noAutoRender = value;
+ assert.equal(element.$.diff.noAutoRender, value);
+ });
+
+ test('passes in path', () => {
+ const value = 'some/file/path';
+ element.path = value;
+ assert.equal(element.$.diff.path, value);
+ });
+
+ test('passes in prefs', () => {
+ const value = createDefaultDiffPrefs();
+ element.prefs = value;
+ assert.equal(element.$.diff.prefs, value);
+ });
+
+ test('passes in displayLine', () => {
+ const value = true;
+ element.displayLine = value;
+ assert.equal(element.$.diff.displayLine, value);
+ });
+
+ test('passes in hidden', () => {
+ const value = true;
+ element.hidden = value;
+ assert.equal(element.$.diff.hidden, value);
+ assert.isNotNull(element.getAttribute('hidden'));
+ });
+
+ test('passes in noRenderOnPrefsChange', () => {
+ const value = true;
+ element.noRenderOnPrefsChange = value;
+ assert.equal(element.$.diff.noRenderOnPrefsChange, value);
+ });
+
+ test('passes in lineWrapping', () => {
+ const value = true;
+ element.lineWrapping = value;
+ assert.equal(element.$.diff.lineWrapping, value);
+ });
+
+ test('passes in viewMode', () => {
+ const value = DiffViewMode.SIDE_BY_SIDE;
+ element.viewMode = value;
+ assert.equal(element.$.diff.viewMode, value);
+ });
+
+ test('passes in lineOfInterest', () => {
+ const value = {lineNum: 123, side: Side.LEFT};
+ element.lineOfInterest = value;
+ assert.equal(element.$.diff.lineOfInterest, value);
+ });
+
+ suite('_reportDiff', () => {
+ let reportStub: SinonStub;
+
+ setup(async () => {
+ element = basicFixture.instantiate();
+ element.changeNum = 123 as NumericChangeId;
+ element.path = 'file.txt';
+ element.patchRange = createPatchRange(1, 2);
+ reportStub = sinon.stub(element.reporting, 'reportInteraction');
+ await flush();
+ });
+
+ test('undefined', () => {
+ element._reportDiff(undefined);
+ assert.isFalse(reportStub.called);
+ });
+
+ test('diff w/ no delta', () => {
+ const diff: DiffInfo = {
+ ...createDiff(),
+ content: [{ab: ['foo', 'bar']}, {ab: ['baz', 'foo']}],
+ };
+ element._reportDiff(diff);
+ assert.isTrue(reportStub.calledOnce);
+ assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero');
+ assert.isUndefined(reportStub.lastCall.args[1]);
+ });
+
+ test('diff w/ no rebase delta', () => {
+ const diff: DiffInfo = {
+ ...createDiff(),
+ content: [
+ {ab: ['foo', 'bar']},
+ {a: ['baz', 'foo']},
+ {ab: ['foo', 'bar']},
+ {a: ['baz', 'foo'], b: ['bar', 'baz']},
+ {ab: ['foo', 'bar']},
+ {b: ['baz', 'foo']},
+ {ab: ['foo', 'bar']},
+ ],
+ };
+ element._reportDiff(diff);
+ assert.isTrue(reportStub.calledOnce);
+ assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero');
+ assert.isUndefined(reportStub.lastCall.args[1]);
+ });
+
+ test('diff w/ some rebase delta', () => {
+ const diff: DiffInfo = {
+ ...createDiff(),
+ content: [
+ {ab: ['foo', 'bar']},
+ {a: ['baz', 'foo'], due_to_rebase: true},
+ {ab: ['foo', 'bar']},
+ {a: ['baz', 'foo'], b: ['bar', 'baz']},
+ {ab: ['foo', 'bar']},
+ {b: ['baz', 'foo'], due_to_rebase: true},
+ {ab: ['foo', 'bar']},
+ {a: ['baz', 'foo']},
+ ],
+ };
+ element._reportDiff(diff);
+ assert.isTrue(reportStub.calledOnce);
+ assert.isTrue(
+ reportStub.calledWith('rebase-percent-nonzero', {
+ percentRebaseDelta: 50,
+ })
+ );
+ });
+
+ test('diff w/ all rebase delta', () => {
+ const diff: DiffInfo = {
+ ...createDiff(),
+ content: [
+ {
+ a: ['foo', 'bar'],
+ b: ['baz', 'foo'],
+ due_to_rebase: true,
+ },
+ ],
+ };
+ element._reportDiff(diff);
+ assert.isTrue(reportStub.calledOnce);
+ assert.isTrue(
+ reportStub.calledWith('rebase-percent-nonzero', {
+ percentRebaseDelta: 100,
+ })
+ );
+ });
+
+ test('diff against parent event', () => {
+ element.patchRange = createPatchRange();
+ const diff: DiffInfo = {
+ ...createDiff(),
+ content: [
+ {
+ a: ['foo', 'bar'],
+ b: ['baz', 'foo'],
+ },
+ ],
+ };
+ element._reportDiff(diff);
+ assert.isTrue(reportStub.calledOnce);
+ assert.equal(reportStub.lastCall.args[0], 'diff-against-parent');
+ assert.isUndefined(reportStub.lastCall.args[1]);
+ });
+ });
+
+ suite('createCheckEl method', () => {
+ test('start_line:12', () => {
+ const result: RunResult = {
+ ...createRunResult(),
+ codePointers: [{path: 'a', range: {start_line: 12} as CommentRange}],
+ };
+ const el = element.createCheckEl(result);
+ assert.equal(el.getAttribute('slot'), 'right-12');
+ assert.equal(el.getAttribute('diff-side'), 'right');
+ assert.equal(el.getAttribute('line-num'), '12');
+ assert.equal(el.getAttribute('range'), null);
+ assert.equal(el.result, result);
+ });
+
+ test('start_line:13 end_line:14 without char positions', () => {
+ const result: RunResult = {
+ ...createRunResult(),
+ codePointers: [
+ {path: 'a', range: {start_line: 13, end_line: 14} as CommentRange},
+ ],
+ };
+ const el = element.createCheckEl(result);
+ assert.equal(el.getAttribute('slot'), 'right-14');
+ assert.equal(el.getAttribute('diff-side'), 'right');
+ assert.equal(el.getAttribute('line-num'), '14');
+ assert.equal(el.getAttribute('range'), null);
+ assert.equal(el.result, result);
+ });
+
+ test('start_line:13 end_line:14 with char positions', () => {
+ const result: RunResult = {
+ ...createRunResult(),
+ codePointers: [
+ {
+ path: 'a',
+ range: {
+ start_line: 13,
+ end_line: 14,
+ start_character: 5,
+ end_character: 7,
+ },
+ },
+ ],
+ };
+ const el = element.createCheckEl(result);
+ assert.equal(el.getAttribute('slot'), 'right-14');
+ assert.equal(el.getAttribute('diff-side'), 'right');
+ assert.equal(el.getAttribute('line-num'), '14');
+ assert.equal(
+ el.getAttribute('range'),
+ '{"start_line":13,' +
+ '"end_line":14,' +
+ '"start_character":5,' +
+ '"end_character":7}'
+ );
+ assert.equal(el.result, result);
+ });
+
+ test('empty range', () => {
+ const result: RunResult = {
+ ...createRunResult(),
+ codePointers: [{path: 'a', range: {} as CommentRange}],
+ };
+ const el = element.createCheckEl(result);
+ assert.equal(el.getAttribute('slot'), 'right-FILE');
+ assert.equal(el.getAttribute('diff-side'), 'right');
+ assert.equal(el.getAttribute('line-num'), 'FILE');
+ assert.equal(el.getAttribute('range'), null);
+ assert.equal(el.result, result);
+ });
+ });
+
+ suite('create-comment', () => {
+ setup(async () => {
+ loggedIn = true;
+ element.connectedCallback();
+ await flush();
+ });
+
+ test('creates comments if they do not exist yet', async () => {
+ element.patchRange = createPatchRange();
+ element.dispatchEvent(
+ new CustomEvent('create-comment', {
+ detail: {
+ lineNum: 3,
+ side: Side.LEFT,
+ path: '/p',
+ },
+ })
+ );
+
+ let threads =
+ element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+
+ assert.equal(threads.length, 1);
+ assert.equal(threads[0].thread?.commentSide, CommentSide.PARENT);
+ assert.equal(threads[0].getAttribute('diff-side'), Side.LEFT);
+ assert.equal(threads[0].thread?.range, undefined);
+ assert.equal(threads[0].thread?.patchNum, 1 as PatchSetNum);
+
+ // Try to fetch a thread with a different range.
+ const range = {
+ start_line: 1,
+ start_character: 1,
+ end_line: 1,
+ end_character: 3,
+ };
+ element.patchRange = createPatchRange();
+
+ element.dispatchEvent(
+ new CustomEvent('create-comment', {
+ detail: {
+ lineNum: 1,
+ side: Side.LEFT,
+ path: '/p',
+ range,
+ },
+ })
+ );
+
+ threads =
+ element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+
+ assert.equal(threads.length, 2);
+ assert.equal(threads[0].thread?.commentSide, CommentSide.PARENT);
+ assert.equal(threads[0].getAttribute('diff-side'), Side.LEFT);
+ assert.equal(threads[1].thread?.range, range);
+ assert.equal(threads[1].thread?.patchNum, 1 as PatchSetNum);
+ });
+
+ test('should not be on parent if on the right', async () => {
+ element.patchRange = createPatchRange(2, 3);
+ element.dispatchEvent(
+ new CustomEvent('create-comment', {
+ detail: {
+ side: Side.RIGHT,
+ },
+ })
+ );
+ await flush();
+
+ const threads =
+ element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+ const threadEl = threads[0];
+
+ assert.equal(threadEl.thread?.commentSide, CommentSide.REVISION);
+ assert.equal(threadEl.getAttribute('diff-side'), Side.RIGHT);
+ });
+
+ test('should be on parent if right and base is PARENT', () => {
+ element.patchRange = createPatchRange();
+
+ element.dispatchEvent(
+ new CustomEvent('create-comment', {
+ detail: {
+ side: Side.LEFT,
+ },
+ })
+ );
+
+ const threads =
+ element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+ const threadEl = threads[0];
+
+ assert.equal(threadEl.thread?.commentSide, CommentSide.PARENT);
+ assert.equal(threadEl.getAttribute('diff-side'), Side.LEFT);
+ });
+
+ test('should be on parent if right and base negative', () => {
+ element.patchRange = createPatchRange(-2, 3);
+
+ element.dispatchEvent(
+ new CustomEvent('create-comment', {
+ detail: {
+ side: Side.LEFT,
+ },
+ })
+ );
+
+ const threads =
+ element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+ const threadEl = threads[0];
+
+ assert.equal(threadEl.thread?.commentSide, CommentSide.PARENT);
+ assert.equal(threadEl.getAttribute('diff-side'), Side.LEFT);
+ });
+
+ test('should not be on parent otherwise', () => {
+ element.patchRange = createPatchRange(2, 3);
+ element.dispatchEvent(
+ new CustomEvent('create-comment', {
+ detail: {
+ side: Side.LEFT,
+ },
+ })
+ );
+
+ const threads =
+ element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+ const threadEl = threads[0];
+
+ assert.equal(threadEl.thread?.commentSide, 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',
+ async () => {
+ element.patchRange = createPatchRange(2, 3);
+ element.file = {basePath: 'file_renamed.txt', path: element.path ?? ''};
+ await flush();
+
+ element.dispatchEvent(
+ new CustomEvent('create-comment', {
+ detail: {
+ side: Side.LEFT,
+ path: '/p',
+ },
+ })
+ );
+
+ const threads =
+ element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+
+ assert.equal(threads.length, 1);
+ 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',
+ async () => {
+ element.patchRange = createPatchRange(2, 3);
+ element.file = {basePath: 'file_renamed.txt', path: element.path ?? ''};
+ await flush();
+
+ element.dispatchEvent(
+ new CustomEvent('create-comment', {
+ detail: {
+ side: Side.RIGHT,
+ path: '/p',
+ },
+ })
+ );
+
+ const threads =
+ element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+
+ assert.equal(threads.length, 1);
+ 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', async () => {
+ element.patchRange = createPatchRange(2, 3);
+
+ element.file = {basePath: 'file_renamed.txt', path: element.path ?? ''};
+ await flush();
+
+ const comment = {
+ ...createComment(),
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 2,
+ end_character: 2,
+ },
+ patch_set: 3 as PatchSetNum,
+ };
+ const thread = createCommentThread([comment]);
+ element.threads = [thread];
+
+ let threads =
+ element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+
+ assert.equal(threads.length, 1);
+ element.threads = [...element.threads, thread];
+
+ threads =
+ element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+ // Threads have same rootId so element is reused
+ assert.equal(threads.length, 1);
+
+ const newThread = {...thread};
+ newThread.rootId = 'differentRootId' as UrlEncodedCommentId;
+ element.threads = [...element.threads, newThread];
+ threads =
+ element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+ // New thread has a different rootId
+ assert.equal(threads.length, 2);
+ });
+
+ test('unsaved thread changes to draft', async () => {
+ element.patchRange = createPatchRange(2, 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 as PatchSetNum,
+ 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 = createPatchRange();
+ element.file = {basePath: 'file_renamed.txt', path: element.path ?? ''};
+ await flush();
+
+ element.dispatchEvent(
+ new CustomEvent('create-comment', {
+ detail: {
+ side: Side.LEFT,
+ path: '/p',
+ },
+ })
+ );
+
+ const threads =
+ element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+
+ assert.equal(threads.length, 1);
+ 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', () => {
+ const alertSpy = sinon.spy();
+ element.addEventListener('show-alert', alertSpy);
+
+ const diffSide = Side.RIGHT;
+ element.patchRange = {
+ basePatchNum: 3 as BasePatchSetNum,
+ patchNum: EditPatchSetNum,
+ };
+ element.dispatchEvent(
+ new CustomEvent('create-comment', {
+ detail: {
+ side: diffSide,
+ path: '/p',
+ },
+ })
+ );
+
+ const threads =
+ element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+ assert.equal(threads.length, 0);
+ assert.isTrue(alertSpy.called);
+ });
+
+ test('cannot create thread on an edit base', () => {
+ const alertSpy = sinon.spy();
+ element.addEventListener('show-alert', alertSpy);
+
+ const diffSide = Side.LEFT;
+ element.patchRange = {
+ basePatchNum: ParentPatchSetNum,
+ patchNum: EditPatchSetNum,
+ };
+ element.dispatchEvent(
+ new CustomEvent('create-comment', {
+ detail: {
+ side: diffSide,
+ path: '/p',
+ },
+ })
+ );
+
+ const threads =
+ element.$.diff.querySelectorAll<GrCommentThread>('gr-comment-thread');
+ assert.equal(threads.length, 0);
+ assert.isTrue(alertSpy.called);
+ });
+ });
+
+ test('_filterThreadElsForLocation with no threads', () => {
+ const line = {beforeNumber: 3, afterNumber: 5};
+ const threads: GrCommentThread[] = [];
+ assert.deepEqual(
+ element._filterThreadElsForLocation(threads, line, Side.LEFT),
+ []
+ );
+ assert.deepEqual(
+ element._filterThreadElsForLocation(threads, line, Side.RIGHT),
+ []
+ );
+ });
+
+ test('_filterThreadElsForLocation for line comments', () => {
+ const line = {beforeNumber: 3, afterNumber: 5};
+
+ const l3 = document.createElement('gr-comment-thread');
+ l3.setAttribute('line-num', '3');
+ l3.setAttribute('diff-side', Side.LEFT);
+
+ const l5 = document.createElement('gr-comment-thread');
+ l5.setAttribute('line-num', '5');
+ l5.setAttribute('diff-side', Side.LEFT);
+
+ const r3 = document.createElement('gr-comment-thread');
+ r3.setAttribute('line-num', '3');
+ r3.setAttribute('diff-side', Side.RIGHT);
+
+ const r5 = document.createElement('gr-comment-thread');
+ r5.setAttribute('line-num', '5');
+ r5.setAttribute('diff-side', Side.RIGHT);
+
+ const threadEls: GrCommentThread[] = [l3, l5, r3, r5];
+ assert.deepEqual(
+ element._filterThreadElsForLocation(threadEls, line, Side.LEFT),
+ [l3]
+ );
+ assert.deepEqual(
+ element._filterThreadElsForLocation(threadEls, line, Side.RIGHT),
+ [r5]
+ );
+ });
+
+ test('_filterThreadElsForLocation for file comments', () => {
+ const line: LineInfo = {beforeNumber: 'FILE', afterNumber: 'FILE'};
+
+ const l = document.createElement('gr-comment-thread');
+ l.setAttribute('diff-side', Side.LEFT);
+ l.setAttribute('line-num', 'FILE');
+
+ const r = document.createElement('gr-comment-thread');
+ r.setAttribute('diff-side', Side.RIGHT);
+ r.setAttribute('line-num', 'FILE');
+
+ const threadEls: GrCommentThread[] = [l, r];
+ assert.deepEqual(
+ element._filterThreadElsForLocation(threadEls, line, Side.LEFT),
+ [l]
+ );
+ assert.deepEqual(
+ element._filterThreadElsForLocation(threadEls, line, Side.RIGHT),
+ [r]
+ );
+ });
+
+ suite('syntax layer with syntax_highlighting on', () => {
+ setup(() => {
+ const prefs = {
+ ...createDefaultDiffPrefs(),
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ context: -1,
+ syntax_highlighting: true,
+ };
+ element.patchRange = createPatchRange();
+ element.prefs = prefs;
+ element.changeNum = 123 as NumericChangeId;
+ element.change = createChange();
+ element.path = 'some/path';
+ });
+
+ test('gr-diff-host provides syntax highlighting layer', async () => {
+ stubRestApi('getDiff').returns(Promise.resolve(createDiff()));
+ await element.reload();
+ assertIsDefined(element.$.diff.layers);
+ assert.equal(element.$.diff.layers[1], element.syntaxLayer);
+ });
+
+ test('rendering normal-sized diff does not disable syntax', () => {
+ element.diff = createDiff();
+ assert.isTrue(element.syntaxLayer.enabled);
+ });
+
+ test('rendering large diff disables syntax', () => {
+ // Before it renders, set the first diff line to 500 '*' characters.
+ element.diff = {
+ ...createDiff(),
+ content: [
+ {
+ a: [new Array(501).join('*')],
+ },
+ ],
+ };
+ assert.isFalse(element.syntaxLayer.enabled);
+ });
+
+ test('starts syntax layer processing on render event', async () => {
+ const stub = sinon
+ .stub(element.syntaxLayer, 'process')
+ .returns(Promise.resolve());
+ stubRestApi('getDiff').returns(Promise.resolve(createDiff()));
+ await element.reload();
+ element.dispatchEvent(
+ new CustomEvent('render', {bubbles: true, composed: true})
+ );
+ assert.isTrue(stub.called);
+ });
+ });
+
+ suite('syntax layer with syntax_highlighting off', () => {
+ setup(() => {
+ const prefs = {
+ ...createDefaultDiffPrefs(),
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ context: -1,
+ syntax_highlighting: false,
+ };
+ element.diff = createDiff();
+ element.patchRange = createPatchRange();
+ element.change = createChange();
+ element.prefs = prefs;
+ });
+
+ test('gr-diff-host provides syntax highlighting layer', async () => {
+ stubRestApi('getDiff').returns(Promise.resolve(createDiff()));
+ await element.reload();
+ assertIsDefined(element.$.diff.layers);
+ assert.equal(element.$.diff.layers[1], element.syntaxLayer);
+ });
+
+ test('syntax layer should be disabled', () => {
+ assert.isFalse(element.syntaxLayer.enabled);
+ });
+
+ test('still disabled for large diff', () => {
+ // Before it renders, set the first diff line to 500 '*' characters.
+ element.diff = {
+ ...createDiff(),
+ content: [
+ {
+ a: [new Array(501).join('*')],
+ },
+ ],
+ };
+ assert.isFalse(element.syntaxLayer.enabled);
+ });
+ });
+
+ suite('coverage layer', () => {
+ let notifyStub: SinonStub;
+ let coverageProviderStub: SinonStub;
+ let getCoverageAnnotationApisStub: SinonStub;
+ const exampleRanges = [
+ {
+ type: CoverageType.COVERED,
+ side: Side.RIGHT,
+ code_range: {
+ start_line: 1,
+ end_line: 2,
+ },
+ },
+ {
+ type: CoverageType.NOT_COVERED,
+ side: Side.RIGHT,
+ code_range: {
+ start_line: 3,
+ end_line: 4,
+ },
+ },
+ ];
+
+ setup(async () => {
+ notifyStub = sinon.stub();
+ coverageProviderStub = sinon
+ .stub()
+ .returns(Promise.resolve(exampleRanges));
+
+ element = basicFixture.instantiate();
+ getCoverageAnnotationApisStub = sinon
+ .stub(element.jsAPI, 'getCoverageAnnotationApis')
+ .returns(
+ Promise.resolve([
+ {
+ notify: notifyStub,
+ getCoverageProvider() {
+ return coverageProviderStub;
+ },
+ } as unknown as GrAnnotationActionsInterface,
+ ])
+ );
+ element.changeNum = 123 as NumericChangeId;
+ element.change = createChange();
+ element.path = 'some/path';
+ const prefs = {
+ ...createDefaultDiffPrefs(),
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ context: -1,
+ };
+ element.diff = {
+ ...createDiff(),
+ content: [{a: ['foo']}],
+ };
+ element.patchRange = createPatchRange();
+ element.prefs = prefs;
+ stubRestApi('getDiff').returns(Promise.resolve(element.diff));
+ await flush();
+ });
+
+ test('getCoverageAnnotationApis should be called', async () => {
+ await element.reload();
+ assert.isTrue(getCoverageAnnotationApisStub.calledOnce);
+ });
+
+ test('coverageRangeChanged should be called', async () => {
+ await element.reload();
+ assert.equal(notifyStub.callCount, 2);
+ assert.isTrue(
+ notifyStub.calledWithExactly('some/path', 1, 2, Side.RIGHT)
+ );
+ assert.isTrue(
+ notifyStub.calledWithExactly('some/path', 3, 4, Side.RIGHT)
+ );
+ });
+
+ test('provider is called with appropriate params', async () => {
+ element.patchRange = createPatchRange(1, 3);
+
+ await element.reload();
+ assert.isTrue(
+ coverageProviderStub.calledWithExactly(
+ 123,
+ 'some/path',
+ 1,
+ 3,
+ element.change
+ )
+ );
+ });
+
+ test('provider is called with appropriate params - special patchset values', async () => {
+ element.patchRange = createPatchRange();
+
+ await element.reload();
+ assert.isTrue(
+ coverageProviderStub.calledWithExactly(
+ 123,
+ 'some/path',
+ undefined,
+ 1,
+ element.change
+ )
+ );
+ });
+ });
+
+ suite('trailing newlines', () => {
+ setup(() => {});
+
+ suite('_lastChunkForSide', () => {
+ test('deltas', () => {
+ const diff: DiffInfo = {
+ ...createDiff(),
+ content: [
+ {a: ['foo', 'bar'], b: ['baz']},
+ {ab: ['foo', 'bar', 'baz']},
+ {b: ['foo']},
+ ],
+ };
+ assert.equal(element._lastChunkForSide(diff, false), diff.content[2]);
+ assert.equal(element._lastChunkForSide(diff, true), diff.content[1]);
+
+ diff.content.push({a: ['foo'], b: ['bar']});
+ assert.equal(element._lastChunkForSide(diff, false), diff.content[3]);
+ assert.equal(element._lastChunkForSide(diff, true), diff.content[3]);
+ });
+
+ test('addition with a undefined', () => {
+ const diff: DiffInfo = {
+ ...createDiff(),
+ content: [{b: ['foo', 'bar', 'baz']}],
+ };
+ assert.equal(element._lastChunkForSide(diff, false), diff.content[0]);
+ assert.isNull(element._lastChunkForSide(diff, true));
+ });
+
+ test('addition with a empty', () => {
+ const diff: DiffInfo = {
+ ...createDiff(),
+ content: [{a: [], b: ['foo', 'bar', 'baz']}],
+ };
+ assert.equal(element._lastChunkForSide(diff, false), diff.content[0]);
+ assert.isNull(element._lastChunkForSide(diff, true));
+ });
+
+ test('deletion with b undefined', () => {
+ const diff: DiffInfo = {
+ ...createDiff(),
+ content: [{a: ['foo', 'bar', 'baz']}],
+ };
+ assert.isNull(element._lastChunkForSide(diff, false));
+ assert.equal(element._lastChunkForSide(diff, true), diff.content[0]);
+ });
+
+ test('deletion with b empty', () => {
+ const diff: DiffInfo = {
+ ...createDiff(),
+ content: [{a: ['foo', 'bar', 'baz'], b: []}],
+ };
+ assert.isNull(element._lastChunkForSide(diff, false));
+ assert.equal(element._lastChunkForSide(diff, true), diff.content[0]);
+ });
+
+ test('empty', () => {
+ const diff: DiffInfo = {...createDiff(), content: []};
+ assert.isNull(element._lastChunkForSide(diff, false));
+ assert.isNull(element._lastChunkForSide(diff, true));
+ });
+ });
+
+ suite('_hasTrailingNewlines', () => {
+ test('shared no trailing', () => {
+ const diff = undefined;
+ sinon.stub(element, '_lastChunkForSide').returns({ab: ['foo', 'bar']});
+ assert.isFalse(element._hasTrailingNewlines(diff, false));
+ assert.isFalse(element._hasTrailingNewlines(diff, true));
+ });
+
+ test('delta trailing in right', () => {
+ const diff = undefined;
+ sinon
+ .stub(element, '_lastChunkForSide')
+ .returns({a: ['foo', 'bar'], b: ['baz', '']});
+ assert.isTrue(element._hasTrailingNewlines(diff, false));
+ assert.isFalse(element._hasTrailingNewlines(diff, true));
+ });
+
+ test('addition', () => {
+ const diff: DiffInfo | undefined = undefined;
+ sinon
+ .stub(element, '_lastChunkForSide')
+ .callsFake((_: DiffInfo | undefined, leftSide: boolean) => {
+ if (leftSide) {
+ return null;
+ }
+ return {b: ['foo', '']};
+ });
+ assert.isTrue(element._hasTrailingNewlines(diff, false));
+ assert.isNull(element._hasTrailingNewlines(diff, true));
+ });
+
+ test('deletion', () => {
+ const diff: DiffInfo | undefined = undefined;
+ sinon
+ .stub(element, '_lastChunkForSide')
+ .callsFake((_: DiffInfo | undefined, leftSide: boolean) => {
+ if (!leftSide) {
+ return null;
+ }
+ return {a: ['foo']};
+ });
+ assert.isNull(element._hasTrailingNewlines(diff, false));
+ assert.isFalse(element._hasTrailingNewlines(diff, true));
+ });
+ });
+ });
+});
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
index dc501c8..2e48771 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
@@ -20,7 +20,6 @@
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';
diff --git a/polygerrit-ui/app/elements/lit/incremental-repeat.ts b/polygerrit-ui/app/elements/lit/incremental-repeat.ts
new file mode 100644
index 0000000..cf14cc1
--- /dev/null
+++ b/polygerrit-ui/app/elements/lit/incremental-repeat.ts
@@ -0,0 +1,109 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {directive, AsyncDirective} from 'lit/async-directive.js';
+import {DirectiveParameters, ChildPart} from 'lit/directive.js';
+import {
+ insertPart,
+ setChildPartValue,
+ removePart,
+} from 'lit/directive-helpers.js';
+
+interface RepeatOptions<T> {
+ values: T[];
+ mapFn?: (val: T, idx: number) => unknown;
+ initialCount: number;
+ targetFrameRate?: number;
+ startAt?: number;
+ // TODO: targetFramerate
+}
+
+interface RepeatState<T> {
+ values: T[];
+ mapFn?: (val: T, idx: number) => unknown;
+ startAt: number;
+ incrementAmount: number;
+ lastRenderedAt: number;
+ targetFrameRate: number;
+}
+
+class IncrementalRepeat<T> extends AsyncDirective {
+ private parts: ChildPart[] = [];
+
+ private part!: ChildPart;
+
+ private state!: RepeatState<T>;
+
+ render(options: RepeatOptions<T>) {
+ const values = options.values.slice(
+ options.startAt ?? 0,
+ (options.startAt ?? 0) + options.initialCount
+ );
+ if (options.mapFn) {
+ return values.map(options.mapFn);
+ }
+ return values;
+ }
+
+ override update(part: ChildPart, [options]: DirectiveParameters<this>) {
+ if (
+ options.values !== this.state?.values ||
+ options.mapFn !== this.state?.mapFn
+ ) {
+ if (this.nextScheduledFrameWork !== undefined)
+ cancelAnimationFrame(this.nextScheduledFrameWork);
+ this.nextScheduledFrameWork = requestAnimationFrame(
+ this.animationFrameHandler
+ );
+ this.part = part;
+ for (let i = 0; i < this.parts.length; i++) {
+ removePart(this.parts[i]);
+ }
+ this.parts = [];
+ this.state = {
+ values: options.values,
+ mapFn: options.mapFn,
+ startAt: options.initialCount,
+ incrementAmount: options.initialCount,
+ lastRenderedAt: performance.now(),
+ targetFrameRate: options.targetFrameRate ?? 30,
+ };
+ }
+ return this.render(options);
+ }
+
+ private nextScheduledFrameWork: number | undefined;
+
+ private animationFrameHandler = () => {
+ const now = performance.now();
+ const frameRate = 1000 / (now - this.state.lastRenderedAt);
+ if (frameRate < this.state.targetFrameRate) {
+ // https://en.wikipedia.org/wiki/Additive_increase/multiplicative_decrease
+ this.state.incrementAmount = Math.max(1, this.state.incrementAmount / 2);
+ } else {
+ this.state.incrementAmount++;
+ }
+ this.state.lastRenderedAt = now;
+ const part = insertPart(this.part);
+ this.parts.push(part);
+ setChildPartValue(
+ part,
+ this.render({
+ mapFn: this.state.mapFn,
+ values: this.state.values,
+ initialCount: this.state.incrementAmount,
+ startAt: this.state.startAt,
+ })
+ );
+ this.state.startAt += this.state.incrementAmount;
+ if (this.state.startAt < this.state.values.length) {
+ this.nextScheduledFrameWork = requestAnimationFrame(
+ this.animationFrameHandler
+ );
+ }
+ };
+}
+
+export const incrementalRepeat = directive(IncrementalRepeat);
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 73be9f3..a09cdbc 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
@@ -17,7 +17,6 @@
import '../../shared/gr-button/gr-button';
import {ServerInfo} from '../../../types/common';
import {getAppContext} from '../../../services/app-context';
-import {KnownExperimentId} from '../../../services/flags/flags';
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators';
import {sharedStyles} from '../../../styles/shared-styles';
@@ -136,14 +135,7 @@
if (!this.serverConfig?.change) return true;
if (column === ColumnNames.COMMENTS)
return this.flagsService.isEnabled('comments-column');
- if (column === ColumnNames.STATUS)
- return !this.flagsService.isEnabled(
- KnownExperimentId.SUBMIT_REQUIREMENTS_UI
- );
- if (column === ColumnNames.STATUS2)
- return this.flagsService.isEnabled(
- KnownExperimentId.SUBMIT_REQUIREMENTS_UI
- );
+ if (column === ColumnNames.STATUS) return false;
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 fdea387..42ef8f4 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
@@ -71,12 +71,6 @@
</td>
</tr>
<tr>
- <td><label for="Status"> Status </label></td>
- <td class="checkboxContainer">
- <input checked="" id="Status" name="Status" type="checkbox" />
- </td>
- </tr>
- <tr>
<td><label for="Owner"> Owner </label></td>
<td class="checkboxContainer">
<input checked="" id="Owner" name="Owner" type="checkbox" />
@@ -117,6 +111,12 @@
<input id="Size" name="Size" type="checkbox" />
</td>
</tr>
+ <tr>
+ <td><label for=" Status "> Status </label></td>
+ <td class="checkboxContainer">
+ <input id=" Status " name=" Status " type="checkbox" />
+ </td>
+ </tr>
</tbody>
</table>
</div>`);
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 81661e2..c4bafac 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
@@ -376,9 +376,7 @@
@change=${this.handleToggleDark}
@click=${this.onTapDarkToggle}
></paper-toggle-button>
- <div id="darkThemeToggleLabel">
- Dark theme (the toggle reloads the page)
- </div>
+ <div id="darkThemeToggleLabel">Dark theme</div>
</div>
</section>
<h2
@@ -451,8 +449,8 @@
>Save changes</gr-button
>
</fieldset>
- <gr-edit-preferences id="editPrefs"></gr-edit-preferences>
- <gr-menu-editor></gr-menu-editor>
+ <gr-edit-preferences id="EditPreferences"></gr-edit-preferences>
+ <gr-menu-editor id="Menu"></gr-menu-editor>
<h2
id="ChangeTableColumns"
class=${this.computeHeaderClass(this.changeTableChanged)}
@@ -1161,6 +1159,7 @@
// private but used in test
reloadPage() {
+ fireAlert(this, 'Reloading...');
windowLocationReload();
}
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
index a514f00..6a8f575 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
@@ -177,7 +177,7 @@
>
</paper-toggle-button>
<div id="darkThemeToggleLabel">
- Dark theme (the toggle reloads the page)
+ Dark theme
</div>
</div>
</section>
@@ -369,8 +369,8 @@
Save changes
</gr-button>
</fieldset>
- <gr-edit-preferences id="editPrefs"> </gr-edit-preferences>
- <gr-menu-editor> </gr-menu-editor>
+ <gr-edit-preferences id="EditPreferences"> </gr-edit-preferences>
+ <gr-menu-editor id="Menu"> </gr-menu-editor>
<h2 id="ChangeTableColumns">Change Table Columns</h2>
<fieldset id="changeTableColumns">
<gr-change-table-editor> </gr-change-table-editor>
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 689a9fb..7f20a1a 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
@@ -27,7 +27,6 @@
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators';
import {ClassInfo, classMap} from 'lit/directives/class-map';
-import {KnownExperimentId} from '../../../services/flags/flags';
import {getLabelStatus, hasVoted, LabelStatus} from '../../../utils/label-util';
@customElement('gr-account-chip')
@@ -94,8 +93,6 @@
private readonly restApiService = getAppContext().restApiService;
- private readonly flagsService = getAppContext().flagsService;
-
static override get styles() {
return [
css`
@@ -252,12 +249,7 @@
}
private computeVoteClasses(): ClassInfo {
- if (
- !this.flagsService.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI) ||
- !this.label ||
- !this.account ||
- !hasVoted(this.label, this.account)
- ) {
+ if (!this.label || !this.account || !hasVoted(this.label, this.account)) {
return {};
}
const status = getLabelStatus(this.label, this.vote?.value);
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 48d6998..3fecd63 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
@@ -167,9 +167,6 @@
@property({type: Array})
removableValues?: AccountInput[];
- @property({type: Number})
- maxCount = 0;
-
/**
* Returns suggestion items
*/
@@ -203,7 +200,7 @@
.group {
--account-label-suffix: ' (group)';
}
- .pending-add {
+ .pendingAdd {
font-style: italic;
}
.list {
@@ -234,8 +231,7 @@
</div>
<gr-account-entry
borderless=""
- ?hidden=${(this.maxCount && this.maxCount <= this.accounts.length) ||
- this.readonly}
+ ?hidden=${this.readonly}
id="entry"
.placeholder=${this.placeholder}
@add=${this.handleAdd}
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
index 7b3a93d..26566a3 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
@@ -400,16 +400,6 @@
assert.equal(element.accounts.length, 1);
});
- test('max-count', async () => {
- element.maxCount = 1;
- const acct = makeAccount();
- handleAdd({account: acct, count: 1});
- await element.updateComplete;
- assert.isTrue(
- queryAndAssert<GrAccountEntry>(element, '#entry').hasAttribute('hidden')
- );
- });
-
test('enter text calls suggestions provider', async () => {
const suggestions: Suggestion[] = [
{
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 48f5d09..3181445 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
@@ -22,7 +22,6 @@
import {addShortcut, getEventPath, Key} from '../../../utils/dom-util';
import {getAppContext} from '../../../services/app-context';
import {classMap} from 'lit/directives/class-map';
-import {KnownExperimentId} from '../../../services/flags/flags';
declare global {
interface HTMLElementTagNameMap {
@@ -206,12 +205,6 @@
];
}
- private readonly flagsService = getAppContext().flagsService;
-
- private readonly isSubmitRequirementsUiEnabled = this.flagsService.isEnabled(
- KnownExperimentId.SUBMIT_REQUIREMENTS_UI
- );
-
override render() {
return html`<paper-button
?raised=${!this.link && !this.flatten}
@@ -220,8 +213,7 @@
tabindex="-1"
part="paper-button"
class=${classMap({
- voteChip: this.voteChip && !this.isSubmitRequirementsUiEnabled,
- newVoteChip: this.voteChip && this.isSubmitRequirementsUiEnabled,
+ newVoteChip: this.voteChip,
})}
>
${this.loading ? html`<span class="loadingSpin"></span>` : ''}
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 cb51337..7d52a3c 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
@@ -305,6 +305,8 @@
// We have to wrap boolean values in Boolean() to ensure undefined values
// use false rather than undefined.
return (
+ Boolean(this.originalDiffPrefs?.syntax_highlighting) !==
+ Boolean(this.diffPrefs?.syntax_highlighting) ||
this.originalDiffPrefs?.context !== this.diffPrefs?.context ||
Boolean(this.originalDiffPrefs?.line_wrapping) !==
Boolean(this.diffPrefs?.line_wrapping) ||
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 79c73f1..82528cd 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
@@ -133,19 +133,16 @@
setActionOverflow(type: ActionType, key: string, overflow: boolean) {
this.reporting.trackApi(this.plugin, 'actions', 'setActionOverflow');
- // TODO(TS): remove return, unclear why it was written
this.ensureEl().setActionOverflow(type, key, overflow);
}
setActionPriority(type: ActionType, key: string, priority: ActionPriority) {
this.reporting.trackApi(this.plugin, 'actions', 'setActionPriority');
- // TODO(TS): remove return, unclear why it was written
this.ensureEl().setActionPriority(type, key, priority);
}
setActionHidden(type: ActionType, key: string, hidden: boolean) {
this.reporting.trackApi(this.plugin, 'actions', 'setActionHidden');
- // TODO(TS): remove return, unclear why it was written
this.ensureEl().setActionHidden(type, key, hidden);
}
@@ -156,7 +153,6 @@
remove(key: string) {
this.reporting.trackApi(this.plugin, 'actions', 'remove');
- // TODO(TS): remove return, unclear why it was written
this.ensureEl().removeActionButton(key);
}
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 b1d3914..ee3bec7 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
@@ -18,11 +18,9 @@
import '../../../styles/gr-voting-styles';
import '../../../styles/shared-styles';
import '../gr-vote-chip/gr-vote-chip';
-import '../gr-account-label/gr-account-label';
import '../gr-account-chip/gr-account-chip';
import '../gr-button/gr-button';
import '../gr-icons/gr-icons';
-import '../gr-label/gr-label';
import '../gr-tooltip-content/gr-tooltip-content';
import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
import {
@@ -30,9 +28,7 @@
LabelInfo,
ApprovalInfo,
AccountId,
- isQuickLabelInfo,
isDetailedLabelInfo,
- LabelNameToInfoMap,
} from '../../../types/common';
import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators';
@@ -40,10 +36,8 @@
import {
canVote,
getApprovalInfo,
- getVotingRangeOrDefault,
hasNeutralStatus,
hasVoted,
- showNewSubmitRequirements,
valueString,
} from '../../../utils/label-util';
import {getAppContext} from '../../../services/app-context';
@@ -61,19 +55,6 @@
}
}
-enum LabelClassName {
- NEGATIVE = 'negative',
- POSITIVE = 'positive',
- MIN = 'min',
- MAX = 'max',
-}
-
-interface FormattedLabel {
- className?: LabelClassName;
- account: ApprovalInfo | AccountInfo;
- value: string;
-}
-
@customElement('gr-label-info')
export class GrLabelInfo extends LitElement {
@property({type: Object})
@@ -107,8 +88,6 @@
private readonly reporting = getAppContext().reportingService;
- private readonly flagsService = getAppContext().flagsService;
-
// TODO(TS): not used, remove later
_xhrPromise?: Promise<void>;
@@ -118,9 +97,6 @@
fontStyles,
votingStyles,
css`
- .placeholder {
- color: var(--deemphasized-text-color);
- }
.hidden {
display: none;
}
@@ -132,33 +108,6 @@
margin-right: var(--spacing-s);
padding: 1px;
}
- .max {
- background-color: var(--vote-color-approved);
- }
- .min {
- background-color: var(--vote-color-rejected);
- }
- .positive {
- background-color: var(--vote-color-recommended);
- border-radius: 12px;
- border: 1px solid var(--vote-outline-recommended);
- color: var(--chip-color);
- }
- .negative {
- background-color: var(--vote-color-disliked);
- border-radius: 12px;
- border: 1px solid var(--vote-outline-disliked);
- color: var(--chip-color);
- }
- .hidden {
- display: none;
- }
- td {
- vertical-align: top;
- }
- tr {
- min-height: var(--line-height-normal);
- }
gr-tooltip-content {
display: block;
}
@@ -173,17 +122,10 @@
gr-button[disabled] iron-icon {
color: var(--border-color);
}
- gr-account-label {
- --account-max-length: 100px;
- margin-right: var(--spacing-xs);
- }
iron-icon {
height: calc(var(--line-height-normal) - 2px);
width: calc(var(--line-height-normal) - 2px);
}
- .labelValueContainer:not(:first-of-type) td {
- padding-top: var(--spacing-s);
- }
.reviewer-row {
padding-top: var(--spacing-s);
}
@@ -208,14 +150,6 @@
}
override render() {
- if (showNewSubmitRequirements(this.flagsService, this.change)) {
- return this.renderNewSubmitRequirements();
- } else {
- return this.renderOldSubmitRequirements();
- }
- }
-
- private renderNewSubmitRequirements() {
const labelInfo = this.labelInfo;
if (!labelInfo) return;
const reviewers = (this.change?.reviewers['REVIEWER'] ?? [])
@@ -238,23 +172,6 @@
</div>`;
}
- private renderOldSubmitRequirements() {
- const labelInfo = this.labelInfo;
- return html` <p
- class="placeholder ${this.computeShowPlaceholder(
- labelInfo,
- this.change?.labels
- )}"
- >
- No votes
- </p>
- <table>
- ${this.mapLabelInfo(labelInfo, this.account, this.change?.labels).map(
- mappedLabel => this.renderLabel(mappedLabel)
- )}
- </table>`;
- }
-
renderReviewerVote(reviewer: AccountInfo) {
const labelInfo = this.labelInfo;
if (!labelInfo) return;
@@ -285,30 +202,6 @@
</div>`;
}
- renderLabel(mappedLabel: FormattedLabel) {
- const {labelInfo, change} = this;
- return html` <tr class="labelValueContainer">
- <td>
- <gr-tooltip-content
- has-tooltip
- title=${this._computeValueTooltip(labelInfo, mappedLabel.value)}
- >
- <gr-label class="${mappedLabel.className} voteChip font-small">
- ${mappedLabel.value}
- </gr-label>
- </gr-tooltip-content>
- </td>
- <td>
- <gr-account-label
- clickable
- .account=${mappedLabel.account}
- .change=${change}
- ></gr-account-label>
- </td>
- <td>${this.renderRemoveVote(mappedLabel.account)}</td>
- </tr>`;
- }
-
private renderVoteAbility(reviewer: AccountInfo) {
if (this.labelInfo && isDetailedLabelInfo(this.labelInfo)) {
const approvalInfo = getApprovalInfo(this.labelInfo, reviewer);
@@ -341,83 +234,6 @@
}
/**
- * This method also listens on change.labels.*,
- * to trigger computation when a label is removed from the change.
- *
- * The third parameter is just for *triggering* computation.
- */
- private mapLabelInfo(
- labelInfo?: LabelInfo,
- account?: AccountInfo,
- _?: LabelNameToInfoMap
- ): FormattedLabel[] {
- const result: FormattedLabel[] = [];
- if (!labelInfo) {
- return result;
- }
- if (!isDetailedLabelInfo(labelInfo)) {
- if (
- isQuickLabelInfo(labelInfo) &&
- (labelInfo.rejected || labelInfo.approved)
- ) {
- const ok = labelInfo.approved || !labelInfo.rejected;
- return [
- {
- value: ok ? '👍️' : '👎️',
- className: ok ? LabelClassName.POSITIVE : LabelClassName.NEGATIVE,
- // executed only if approved or rejected is not undefined
- account: ok ? labelInfo.approved! : labelInfo.rejected!,
- },
- ];
- }
- return result;
- }
-
- // Sort votes by positivity.
- // TODO(TS): maybe mark value as required if always present
- const votes = (labelInfo.all || []).sort(
- (a, b) => (a.value || 0) - (b.value || 0)
- );
- const votingRange = getVotingRangeOrDefault(labelInfo);
- for (const label of votes) {
- if (
- label.value &&
- (!isQuickLabelInfo(labelInfo) ||
- label.value !== labelInfo.default_value)
- ) {
- let labelClassName;
- let labelValPrefix = '';
- if (label.value > 0) {
- labelValPrefix = '+';
- if (label.value === votingRange.max) {
- labelClassName = LabelClassName.MAX;
- } else {
- labelClassName = LabelClassName.POSITIVE;
- }
- } else if (label.value < 0) {
- if (label.value === votingRange.min) {
- labelClassName = LabelClassName.MIN;
- } else {
- labelClassName = LabelClassName.NEGATIVE;
- }
- }
- const formattedLabel: FormattedLabel = {
- value: `${labelValPrefix}${label.value}`,
- className: labelClassName,
- account: label,
- };
- if (label._account_id === account?._account_id) {
- // Put self-votes at the top.
- result.unshift(formattedLabel);
- } else {
- result.push(formattedLabel);
- }
- }
- }
- return result;
- }
-
- /**
* A user is able to delete a vote iff the mutable property is true and the
* reviewer that left the vote exists in the list of removable_reviewers
* received from the backend.
@@ -488,39 +304,4 @@
}
return labelInfo.values[score];
}
-
- /**
- * This method also listens change.labels.* in
- * order to trigger computation when a label is removed from the change.
- *
- * The second parameter is just for *triggering* computation.
- */
- private computeShowPlaceholder(
- labelInfo?: LabelInfo,
- _?: LabelNameToInfoMap
- ) {
- if (!labelInfo) {
- return '';
- }
- if (
- !isDetailedLabelInfo(labelInfo) &&
- isQuickLabelInfo(labelInfo) &&
- (labelInfo.rejected || labelInfo.approved)
- ) {
- return 'hidden';
- }
-
- if (isDetailedLabelInfo(labelInfo) && labelInfo.all) {
- for (const label of labelInfo.all) {
- if (
- label.value &&
- (!isQuickLabelInfo(labelInfo) ||
- label.value !== labelInfo.default_value)
- ) {
- return 'hidden';
- }
- }
- }
- return '';
- }
}
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
index 0ac49a7..f1336b4 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.ts
@@ -20,20 +20,18 @@
import {
isHidden,
mockPromise,
- queryAll,
queryAndAssert,
stubRestApi,
} from '../../../test/test-utils';
import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
import {GrLabelInfo} from './gr-label-info';
import {GrButton} from '../gr-button/gr-button';
-import {GrLabel} from '../gr-label/gr-label';
import {
createAccountWithIdNameAndEmail,
+ createDetailedLabelInfo,
createParsedChange,
} from '../../../test/test-data-generators';
-import {LabelInfo} from '../../../types/common';
-import {GrAccountLabel} from '../gr-account-label/gr-account-label';
+import {ApprovalInfo, LabelInfo} from '../../../types/common';
const basicFixture = fixtureFromElement('gr-label-info');
@@ -41,12 +39,51 @@
let element: GrLabelInfo;
const account = createAccountWithIdNameAndEmail(5);
- setup(() => {
+ setup(async () => {
element = basicFixture.instantiate();
// Needed to trigger computed bindings.
element.account = {};
- element.change = {...createParsedChange(), labels: {}};
+ element.change = {
+ ...createParsedChange(),
+ labels: {},
+ reviewers: {
+ REVIEWER: [account],
+ CC: [],
+ },
+ };
+ const approval: ApprovalInfo = {
+ value: 2,
+ _account_id: account._account_id,
+ };
+ element.labelInfo = {
+ ...createDetailedLabelInfo(),
+ all: [approval],
+ };
+ await element.updateComplete;
+ });
+
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(/* HTML */ `<div>
+ <div class="reviewer-row">
+ <gr-account-chip>
+ <gr-vote-chip circle-shape="" slot="vote-chip"> </gr-vote-chip>
+ </gr-account-chip>
+ <gr-tooltip-content has-tooltip="" title="Remove vote">
+ <gr-button
+ aria-disabled="false"
+ aria-label="Remove vote"
+ class="deleteBtn hidden"
+ data-account-id="5"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ <iron-icon icon="gr-icons:delete"> </iron-icon>
+ </gr-button>
+ </gr-tooltip-content>
+ </div>
+ </div>`);
});
suite('remove reviewer votes', () => {
@@ -62,6 +99,10 @@
element.change = {
...createParsedChange(),
labels: {'Code-Review': label},
+ reviewers: {
+ REVIEWER: [account],
+ CC: [],
+ },
};
element.labelInfo = label;
element.label = 'Code-Review';
@@ -108,101 +149,6 @@
});
});
- suite('label color and order', () => {
- test('valueless label rejected', async () => {
- element.labelInfo = {rejected: {name: 'someone'}};
- await element.updateComplete;
- const labels = queryAll<GrLabel>(element, 'gr-label');
- assert.isTrue(labels[0].classList.contains('negative'));
- });
-
- test('valueless label approved', async () => {
- element.labelInfo = {approved: {name: 'someone'}};
- await element.updateComplete;
- const labels = queryAll<GrLabel>(element, 'gr-label');
- assert.isTrue(labels[0].classList.contains('positive'));
- });
-
- test('-2 to +2', async () => {
- element.labelInfo = {
- all: [
- {value: 2, name: 'user 2'},
- {value: 1, name: 'user 1'},
- {value: -1, name: 'user 3'},
- {value: -2, name: 'user 4'},
- ],
- values: {
- '-2': 'Awful',
- '-1': "Don't submit as-is",
- ' 0': 'No score',
- '+1': 'Looks good to me',
- '+2': 'Ready to submit',
- },
- };
- await element.updateComplete;
- const labels = queryAll<GrLabel>(element, 'gr-label');
- assert.isTrue(labels[0].classList.contains('max'));
- assert.isTrue(labels[1].classList.contains('positive'));
- assert.isTrue(labels[2].classList.contains('negative'));
- assert.isTrue(labels[3].classList.contains('min'));
- });
-
- test('-1 to +1', async () => {
- element.labelInfo = {
- all: [
- {value: 1, name: 'user 1'},
- {value: -1, name: 'user 2'},
- ],
- values: {
- '-1': "Don't submit as-is",
- ' 0': 'No score',
- '+1': 'Looks good to me',
- },
- };
- await element.updateComplete;
- const labels = queryAll<GrLabel>(element, 'gr-label');
- assert.isTrue(labels[0].classList.contains('max'));
- assert.isTrue(labels[1].classList.contains('min'));
- });
-
- test('0 to +2', async () => {
- element.labelInfo = {
- all: [
- {value: 1, name: 'user 2'},
- {value: 2, name: 'user '},
- ],
- values: {
- ' 0': "Don't submit as-is",
- '+1': 'No score',
- '+2': 'Looks good to me',
- },
- };
- await element.updateComplete;
- const labels = queryAll<GrLabel>(element, 'gr-label');
- assert.isTrue(labels[0].classList.contains('max'));
- assert.isTrue(labels[1].classList.contains('positive'));
- });
-
- test('self votes at top', async () => {
- const otherAccount = createAccountWithIdNameAndEmail(8);
- element.account = account;
- element.labelInfo = {
- all: [
- {...otherAccount, value: 1},
- {...account, value: -1},
- ],
- values: {
- '-1': "Don't submit as-is",
- ' 0': 'No score',
- '+1': 'Looks good to me',
- },
- };
- await element.updateComplete;
- const chips = queryAll<GrAccountLabel>(element, 'gr-account-label');
- assert.equal(chips[0].account!._account_id, element.account._account_id);
- });
- });
-
test('_computeValueTooltip', () => {
// Existing label.
let labelInfo: LabelInfo = {values: {0: 'Baz'}};
@@ -218,49 +164,4 @@
score = '0';
assert.equal(element._computeValueTooltip(labelInfo, score), '');
});
-
- test('placeholder', async () => {
- const values = {
- '0': 'No score',
- '+1': 'good',
- '+2': 'excellent',
- '-1': 'bad',
- '-2': 'terrible',
- };
- element.labelInfo = {};
- await element.updateComplete;
- assert.isFalse(
- isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
- );
- element.labelInfo = {all: [], values};
- await element.updateComplete;
- assert.isFalse(
- isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
- );
- element.labelInfo = {all: [{value: 1}], values};
- await element.updateComplete;
- assert.isTrue(
- isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
- );
- element.labelInfo = {rejected: account};
- await element.updateComplete;
- assert.isTrue(
- isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
- );
- element.labelInfo = {rejected: account, all: [{value: 1}], values};
- await element.updateComplete;
- assert.isTrue(
- isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
- );
- element.labelInfo = {approved: account};
- await element.updateComplete;
- assert.isTrue(
- isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
- );
- element.labelInfo = {approved: account, all: [{value: 1}], values};
- await element.updateComplete;
- assert.isTrue(
- isHidden(queryAndAssert<HTMLParagraphElement>(element, '.placeholder'))
- );
- });
});
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label.ts b/polygerrit-ui/app/elements/shared/gr-label/gr-label.ts
deleted file mode 100644
index 842b35e..0000000
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label.ts
+++ /dev/null
@@ -1,42 +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.
- */
-
-/**
- * @fileoverview Consider removing this element as
- * its functionality seems to be duplicated with gr-tooltip and only
- * used in gr-label-info.
- */
-
-import {html, LitElement} from 'lit';
-import {customElement} from 'lit/decorators';
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-label': GrLabel;
- }
-}
-
-@customElement('gr-label')
-export class GrLabel extends LitElement {
- static override get styles() {
- return [];
- }
-
- override render() {
- return html` <slot></slot> `;
- }
-}
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.ts b/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.ts
deleted file mode 100644
index 94196df..0000000
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.ts
+++ /dev/null
@@ -1,19 +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` <slot></slot> `;
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 146a01e..5859731 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
@@ -23,8 +23,6 @@
isQuickLabelInfo,
LabelInfo,
} from '../../../api/rest-api';
-import {getAppContext} from '../../../services/app-context';
-import {KnownExperimentId} from '../../../services/flags/flags';
import {
classForLabelStatus,
getLabelStatus,
@@ -61,8 +59,6 @@
@property({type: Boolean, attribute: 'tooltip-with-who-voted'})
tooltipWithWhoVoted = false;
- private readonly flagsService = getAppContext().flagsService;
-
static override get styles() {
return [
css`
@@ -131,9 +127,6 @@
}
override render() {
- if (!this.flagsService.isEnabled(KnownExperimentId.SUBMIT_REQUIREMENTS_UI))
- return;
-
const renderValue = this.renderValue();
if (!renderValue) return;
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
index a5effaf..47f5f81 100644
--- a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
+++ b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
@@ -462,7 +462,7 @@
numLines: number,
referenceLine: number
) {
- assertIsDefined(this.diff, 'diff');
+ if (!this.diff?.meta_b) return;
const syntaxTree = this.diff.meta_b.syntax_tree;
const outlineSyntaxPath = findBlockTreePathForLine(
referenceLine,
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
index 269b56d..25d3398 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element.ts
@@ -1,24 +1,11 @@
/**
* @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.
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
import '../gr-diff-processor/gr-diff-processor';
import '../../../elements/shared/gr-hovercard/gr-hovercard';
import './gr-diff-builder-side-by-side';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-diff-builder-element_html';
import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
import {DiffBuilder, DiffContextExpandedEventDetail} from './gr-diff-builder';
import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side';
@@ -26,7 +13,6 @@
import {GrDiffBuilderUnified} from './gr-diff-builder-unified';
import {GrDiffBuilderBinary} from './gr-diff-builder-binary';
import {CancelablePromise, makeCancelable} from '../../../scripts/util';
-import {customElement, property, observe} from '@polymer/decorators';
import {BlameInfo, ImageInfo} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
import {CoverageRange, DiffLayer} from '../../../types/types';
@@ -40,11 +26,7 @@
GrRangedCommentLayer,
} from '../gr-ranged-comment-layer/gr-ranged-comment-layer';
import {GrCoverageLayer} from '../gr-coverage-layer/gr-coverage-layer';
-import {
- DiffViewMode,
- RenderPreferences,
- RenderProgressEventDetail,
-} from '../../../api/diff';
+import {DiffViewMode, RenderPreferences} from '../../../api/diff';
import {createDefaultDiffPrefs, Side} from '../../../constants/constants';
import {GrDiffLine, LineNumber} from '../gr-diff/gr-diff-line';
import {
@@ -53,18 +35,26 @@
hideInContextControl,
} from '../gr-diff/gr-diff-group';
import {getLineNumber, getSideByLineEl} from '../gr-diff/gr-diff-utils';
-import {fireAlert, fireEvent, fire} from '../../../utils/event-util';
-import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
+import {fireAlert, fireEvent} from '../../../utils/event-util';
+import {assertIsDefined} from '../../../utils/common-util';
+import {untilRendered} from '../../../utils/dom-util';
const TRAILING_WHITESPACE_PATTERN = /\s+$/;
-
-// https://gerrit.googlesource.com/gerrit/+/234616a8627334686769f1de989d286039f4d6a5/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js#740
const COMMIT_MSG_PATH = '/COMMIT_MSG';
const COMMIT_MSG_LINE_LENGTH = 72;
declare global {
interface HTMLElementEventMap {
- 'render-progress': CustomEvent<RenderProgressEventDetail>;
+ /**
+ * Fired when the diff begins rendering - both for full renders and for
+ * partial rerenders.
+ */
+ 'render-start': CustomEvent<{}>;
+ /**
+ * Fired when the diff finishes rendering text content - both for full
+ * renders and for partial rerenders.
+ */
+ 'render-content': CustomEvent<{}>;
}
}
@@ -97,112 +87,59 @@
}
}
-@customElement('gr-diff-builder')
-export class GrDiffBuilderElement
- extends PolymerElement
- implements GroupConsumer
-{
- static get template() {
- return htmlTemplate;
- }
-
- /**
- * Fired when the diff begins rendering - both for full renders and for
- * partial rerenders.
- *
- * @event render-start
- */
-
- /**
- * Fired whenever a new chunk of lines has been rendered synchronously - this
- * only happens for full renders.
- *
- * @event render-progress
- */
-
- /**
- * Fired when the diff finishes rendering text content - both for full
- * renders and for partial rerenders.
- *
- * @event render-content
- */
-
- @property({type: Object})
+// TODO: Rename the class and the file and remove "element". This is not an
+// element anymore.
+export class GrDiffBuilderElement implements GroupConsumer {
diff?: DiffInfo;
- @property({type: String})
+ diffElement?: HTMLTableElement;
+
viewMode?: string;
- @property({type: Boolean})
isImageDiff?: boolean;
- @property({type: Object})
baseImage: ImageInfo | null = null;
- @property({type: Object})
revisionImage: ImageInfo | null = null;
- @property({type: Number})
- parentIndex?: number;
-
- @property({type: String})
path?: string;
- @property({type: Object})
prefs: DiffPreferencesInfo = createDefaultDiffPrefs();
- @property({type: Object})
renderPrefs?: RenderPreferences;
- @property({type: Object})
- _builder?: DiffBuilder;
-
- /**
- * The gr-diff-processor adds (and only adds!) to this array. It does so by
- * using `this.push()` and Polymer's two-way data binding.
- * Below (@observe('_groups.splices')) we are observing the groups that the
- * processor adds, and pass them on to the builder for rendering. Henceforth
- * the builder groups are the source of truth, because when
- * expanding/collapsing groups only the builder is updated. This field and the
- * corresponsing one in the processor are not updated.
- */
- @property({type: Array})
- _groups: GrDiffGroup[] = [];
+ useNewImageDiffUi = false;
/**
* Layers passed in from the outside.
+ *
+ * See `layersInternal` for where these layers will end up together with the
+ * internal layers.
*/
- @property({type: Array})
layers: DiffLayer[] = [];
+ // visible for testing
+ builder?: DiffBuilder;
+
/**
- * All layers, both from the outside and the default ones.
+ * All layers, both from the outside and the default ones. See `layers` for
+ * the property that can be set from the outside.
*/
- @property({type: Array})
- _layers: DiffLayer[] = [];
+ // visible for testing
+ layersInternal: DiffLayer[] = [];
- @property({type: Boolean})
- _showTabs?: boolean;
+ // visible for testing
+ showTabs?: boolean;
- @property({type: Boolean})
- _showTrailingWhitespace?: boolean;
-
- @property({type: Array})
- commentRanges: CommentRangeLayer[] = [];
-
- @property({type: Array, observer: 'coverageObserver'})
- coverageRanges: CoverageRange[] = [];
-
- @property({type: Boolean})
- useNewImageDiffUi = false;
+ // visible for testing
+ showTrailingWhitespace?: boolean;
/**
* The promise last returned from `render()` while the asynchronous
* rendering is running - `null` otherwise. Provides a `cancel()`
* method that rejects it with `{isCancelled: true}`.
*/
- @property({type: Object})
- _cancelableRenderPromise: CancelablePromise<unknown> | null = null;
+ private cancelableRenderPromise: CancelablePromise<unknown> | null = null;
private coverageLayerLeft = new GrCoverageLayer(Side.LEFT);
@@ -210,51 +147,30 @@
private rangeLayer = new GrRangedCommentLayer();
- private processor = new GrDiffProcessor();
+ // visible for testing
+ processor = new GrDiffProcessor();
+
+ /**
+ * Groups are mostly just passed on to the diff builder (this.builder). But
+ * we also keep track of them here for being able to fire a `render-content`
+ * event when .element of each group has rendered.
+ *
+ * TODO: Refactor DiffBuilderElement and DiffBuilders with a cleaner
+ * separation of responsibilities.
+ */
+ private groups: GrDiffGroup[] = [];
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.replaceGroup(e.detail.contextGroup, e.detail.groups);
- }
- );
- });
this.processor.consumer = this;
}
- override disconnectedCallback() {
- this.processor.cancel();
- if (this._builder) {
- this._builder.clear();
- }
- super.disconnectedCallback();
+ updateCommentRanges(ranges: CommentRangeLayer[]) {
+ this.rangeLayer.updateRanges(ranges);
}
- get diffElement(): HTMLTableElement {
- // Not searching in shadowRoot, because the diff table is slotted!
- return this.querySelector('#diffTable') as HTMLTableElement;
- }
-
- @observe('commentRanges.*')
- rangeObserver() {
- this.rangeLayer.updateRanges(this.commentRanges);
- }
-
- coverageObserver(coverageRanges: CoverageRange[]) {
- const leftRanges = coverageRanges.filter(
- range => range && range.side === Side.LEFT
- );
- this.coverageLayerLeft.setRanges(leftRanges);
-
- const rightRanges = coverageRanges.filter(
- range => range && range.side === Side.RIGHT
- );
- this.coverageLayerRight.setRanges(rightRanges);
+ updateCoverageRanges(rs: CoverageRange[]) {
+ this.coverageLayerLeft.setRanges(rs.filter(r => r?.side === Side.LEFT));
+ this.coverageLayerRight.setRanges(rs.filter(r => r?.side === Side.RIGHT));
}
render(keyLocations: KeyLocations): void {
@@ -262,62 +178,94 @@
// 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.setupAnnotationLayers();
- this._showTabs = this.prefs.show_tabs;
- this._showTrailingWhitespace = this.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();
- if (this._builder) {
- this._builder.clear();
- }
- if (!this.diff) {
- throw Error('Cannot render a diff without DiffInfo.');
- }
- this._builder = this._getDiffBuilder();
+ this.builder?.clear();
+ assertIsDefined(this.diff, 'diff');
+ assertIsDefined(this.diffElement, 'diff table');
+ this.builder = this.getDiffBuilder();
this.processor.context = this.prefs.context;
this.processor.keyLocations = keyLocations;
- this._clearDiffContent();
- this._builder.addColumns(
+ this.diffElement.addEventListener(
+ 'diff-context-expanded',
+ this.onDiffContextExpanded
+ );
+
+ this.clearDiffContent();
+ this.builder.addColumns(
this.diffElement,
getLineNumberCellWidth(this.prefs)
);
const isBinary = !!(this.isImageDiff || this.diff.binary);
- fireEvent(this, 'render-start');
- this._cancelableRenderPromise = makeCancelable(
- this.processor
- .process(this.diff.content, isBinary)
- .then(() => {
- if (this.isImageDiff) {
- (this._builder as GrDiffBuilderImage).renderDiff();
- }
- afterNextRender(this, () => fireEvent(this, 'render-content'));
- })
- // Mocha testing does not like uncaught rejections, so we catch
- // the cancels which are expected and should not throw errors in
- // tests.
- .catch(e => {
- if (!e.isCanceled) return Promise.reject(e);
- return;
- })
- .finally(() => {
- this._cancelableRenderPromise = null;
- })
+ this.fireDiffEvent('render-start');
+ // TODO: processor.process() returns a cancelable promise already.
+ // Why wrap another one around it?
+ this.cancelableRenderPromise = makeCancelable(
+ this.processor.process(this.diff.content, isBinary)
);
+ // All then/catch/finally clauses must be outside of makeCancelable().
+ this.cancelableRenderPromise
+ .then(async () => {
+ if (this.isImageDiff) {
+ (this.builder as GrDiffBuilderImage).renderDiff();
+ }
+ await this.untilGroupsRendered();
+ this.fireDiffEvent('render-content');
+ })
+ // Mocha testing does not like uncaught rejections, so we catch
+ // the cancels which are expected and should not throw errors in
+ // tests.
+ .catch(e => {
+ if (!e.isCanceled) return Promise.reject(e);
+ return;
+ })
+ .finally(() => {
+ this.cancelableRenderPromise = null;
+ });
}
- _setupAnnotationLayers() {
+ // visible for testing
+ async untilGroupsRendered(groups: readonly GrDiffGroup[] = this.groups) {
+ for (const g of groups) {
+ // The LOST or FILE lines may be hidden and thus never resolve an
+ // untilRendered() promise.
+ const lineNumber = g.lines?.[0]?.beforeNumber;
+ if (g.skip || lineNumber === 'LOST' || lineNumber === 'FILE') continue;
+ assertIsDefined(g.element);
+ await untilRendered(g.element);
+ }
+ }
+
+ private onDiffContextExpanded = (
+ e: CustomEvent<DiffContextExpandedEventDetail>
+ ) => {
+ // Don't stop propagation. The host may listen for reporting or
+ // resizing.
+ this.replaceGroup(e.detail.contextGroup, e.detail.groups);
+ };
+
+ private fireDiffEvent<K extends keyof HTMLElementEventMap>(type: K) {
+ assertIsDefined(this.diffElement, 'diff table');
+ fireEvent(this.diffElement, type);
+ }
+
+ // visible for testing
+ setupAnnotationLayers() {
const layers: DiffLayer[] = [
- this._createTrailingWhitespaceLayer(),
- this._createIntralineLayer(),
- this._createTabIndicatorLayer(),
- this._createSpecialCharacterIndicatorLayer(),
+ this.createTrailingWhitespaceLayer(),
+ this.createIntralineLayer(),
+ this.createTabIndicatorLayer(),
+ this.createSpecialCharacterIndicatorLayer(),
this.rangeLayer,
this.coverageLayerLeft,
this.coverageLayerRight,
@@ -326,15 +274,15 @@
if (this.layers) {
layers.push(...this.layers);
}
- this._layers = layers;
+ this.layersInternal = layers;
}
getContentTdByLine(lineNumber: LineNumber, side?: Side, root?: Element) {
- if (!this._builder) return null;
- return this._builder.getContentTdByLine(lineNumber, side, root);
+ if (!this.builder) return null;
+ return this.builder.getContentTdByLine(lineNumber, side, root);
}
- _getDiffRowByChild(child: Element) {
+ private getDiffRowByChild(child: Element) {
while (!child.classList.contains('diff-row') && child.parentElement) {
child = child.parentElement;
}
@@ -348,23 +296,23 @@
const side = getSideByLineEl(lineEl);
// Performance optimization because we already have an element in the
// correct row
- const row = this._getDiffRowByChild(lineEl);
+ const row = this.getDiffRowByChild(lineEl);
return this.getContentTdByLine(line, side, row);
}
getLineElByNumber(lineNumber: LineNumber, side?: Side) {
- if (!this._builder) return null;
- return this._builder.getLineElByNumber(lineNumber, side);
+ if (!this.builder) return null;
+ return this.builder.getLineElByNumber(lineNumber, side);
}
getLineNumberRows() {
- if (!this._builder) return [];
- return this._builder.getLineNumberRows();
+ if (!this.builder) return [];
+ return this.builder.getLineNumberRows();
}
getLineNumEls(side: Side) {
- if (!this._builder) return [];
- return this._builder.getLineNumEls(side);
+ if (!this.builder) return [];
+ return this.builder.getLineNumEls(side);
}
/**
@@ -376,8 +324,8 @@
* @param side The side the line number refer to.
*/
unhideLine(lineNum: number, side: Side) {
- if (!this._builder) return;
- const group = this._builder.findGroup(side, lineNum);
+ if (!this.builder) return;
+ const group = this.builder.findGroup(side, lineNum);
// Cannot unhide a line that is not part of the diff.
if (!group) return;
// If it's already visible, great!
@@ -420,45 +368,47 @@
contextGroup: GrDiffGroup,
newGroups: readonly GrDiffGroup[]
) {
- if (!this._builder) return;
- fireEvent(this, 'render-start');
- const linesRendered = newGroups.reduce(
- (sum, group) => sum + group.lines.length,
- 0
- );
- this._builder.replaceGroup(contextGroup, newGroups);
- afterNextRender(this, () => {
- fire(this, 'render-progress', {linesRendered});
- fireEvent(this, 'render-content');
+ if (!this.builder) return;
+ this.fireDiffEvent('render-start');
+ this.builder.replaceGroup(contextGroup, newGroups);
+ this.groups = this.groups.filter(g => g !== contextGroup);
+ this.groups.push(...newGroups);
+ this.untilGroupsRendered(newGroups).then(() => {
+ this.fireDiffEvent('render-content');
});
}
cancel() {
this.processor.cancel();
- if (this._cancelableRenderPromise) {
- this._cancelableRenderPromise.cancel();
- this._cancelableRenderPromise = null;
- }
+ this.builder?.clear();
+ this.cancelableRenderPromise?.cancel();
+ this.cancelableRenderPromise = null;
+ this.diffElement?.removeEventListener(
+ 'diff-context-expanded',
+ this.onDiffContextExpanded
+ );
}
- _handlePreferenceError(pref: string): never {
+ // visible for testing
+ handlePreferenceError(pref: string): never {
const message =
`The value of the '${pref}' user preference is ` +
'invalid. Fix in diff preferences';
- fireAlert(this, message);
+ assertIsDefined(this.diffElement, 'diff table');
+ fireAlert(this.diffElement, message);
throw Error(`Invalid preference value: ${pref}`);
}
- _getDiffBuilder(): DiffBuilder {
- if (!this.diff) {
- throw Error('Cannot render a diff without DiffInfo.');
- }
+ // visible for testing
+ getDiffBuilder(): DiffBuilder {
+ assertIsDefined(this.diff, 'diff');
+ assertIsDefined(this.diffElement, 'diff table');
if (isNaN(this.prefs.tab_size) || this.prefs.tab_size <= 0) {
- this._handlePreferenceError('tab size');
+ this.handlePreferenceError('tab size');
}
if (isNaN(this.prefs.line_length) || this.prefs.line_length <= 0) {
- this._handlePreferenceError('diff width');
+ this.handlePreferenceError('diff width');
}
const localPrefs = {...this.prefs};
@@ -487,7 +437,7 @@
this.diff,
localPrefs,
this.diffElement,
- this._layers,
+ this.layersInternal,
this.renderPrefs
);
} else if (this.viewMode === DiffViewMode.UNIFIED) {
@@ -495,7 +445,7 @@
this.diff,
localPrefs,
this.diffElement,
- this._layers,
+ this.layersInternal,
this.renderPrefs
);
}
@@ -505,7 +455,8 @@
return builder;
}
- _clearDiffContent() {
+ private clearDiffContent() {
+ assertIsDefined(this.diffElement, 'diff table');
this.diffElement.innerHTML = '';
}
@@ -514,22 +465,22 @@
* server into chunks.
*/
clearGroups() {
- if (!this._builder) return;
- this._builder.clearGroups();
+ if (!this.builder) return;
+ this.groups = [];
+ this.builder.clearGroups();
}
/**
* Called when the processor is done converting a chunk of the diff.
*/
addGroup(group: GrDiffGroup) {
- if (!this._builder) return;
- this._builder.addGroups([group]);
- afterNextRender(this, () =>
- fire(this, 'render-progress', {linesRendered: group.lines.length})
- );
+ if (!this.builder) return;
+ this.builder.addGroups([group]);
+ this.groups.push(group);
}
- _createIntralineLayer(): DiffLayer {
+ // visible for testing
+ createIntralineLayer(): DiffLayer {
return {
// Take a DIV.contentText element and a line object with intraline
// differences to highlight and apply them to the element as
@@ -561,8 +512,9 @@
};
}
- _createTabIndicatorLayer(): DiffLayer {
- const show = () => this._showTabs;
+ // visible for testing
+ createTabIndicatorLayer(): DiffLayer {
+ const show = () => this.showTabs;
return {
annotate(contentEl: HTMLElement, _: HTMLElement, line: GrDiffLine) {
// If visible tabs are disabled, do nothing.
@@ -576,7 +528,7 @@
};
}
- _createSpecialCharacterIndicatorLayer(): DiffLayer {
+ private createSpecialCharacterIndicatorLayer(): DiffLayer {
return {
annotate(contentEl: HTMLElement, _: HTMLElement, line: GrDiffLine) {
// Find and annotate the locations of soft hyphen (\u00AD)
@@ -592,8 +544,9 @@
};
}
- _createTrailingWhitespaceLayer(): DiffLayer {
- const show = () => this._showTrailingWhitespace;
+ // visible for testing
+ createTrailingWhitespaceLayer(): DiffLayer {
+ const show = () => this.showTrailingWhitespace;
return {
annotate(contentEl: HTMLElement, _: HTMLElement, line: GrDiffLine) {
@@ -621,18 +574,12 @@
}
setBlame(blame: BlameInfo[] | null) {
- if (!this._builder) return;
- this._builder.setBlame(blame ?? []);
+ if (!this.builder) return;
+ this.builder.setBlame(blame ?? []);
}
updateRenderPrefs(renderPrefs: RenderPreferences) {
- this._builder?.updateRenderPrefs(renderPrefs);
+ this.builder?.updateRenderPrefs(renderPrefs);
this.processor.updateRenderPrefs(renderPrefs);
}
}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-diff-builder': GrDiffBuilderElement;
- }
-}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_html.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_html.ts
deleted file mode 100644
index bd0e034..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_html.ts
+++ /dev/null
@@ -1,23 +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`
- <div class="contentWrapper">
- <slot></slot>
- </div>
-`;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.js b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.js
deleted file mode 100644
index 8c15ddd..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.js
+++ /dev/null
@@ -1,1084 +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 {createDiff} from '../../../test/test-data-generators.js';
-import './gr-diff-builder-element.js';
-import {stubBaseUrl} from '../../../test/test-utils.js';
-import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
-import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line.js';
-import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-import {DiffViewMode, Side} from '../../../api/diff.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
-import {GrDiffBuilderLegacy} from './gr-diff-builder-legacy.js';
-import {waitForEventOnce} from '../../../utils/event-util.js';
-
-const basicFixture = fixtureFromTemplate(html`
- <gr-diff-builder>
- <table id="diffTable"></table>
- </gr-diff-builder>
-`);
-
-const divWithTextFixture = fixtureFromTemplate(html`
-<div>Lorem ipsum dolor sit amet, suspendisse inceptos vehicula</div>
-`);
-
-const mockDiffFixture = fixtureFromTemplate(html`
-<gr-diff-builder view-mode="SIDE_BY_SIDE">
- <table id="diffTable"></table>
- </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;
- let builder;
-
- const LINE_BREAK_HTML = '<span class="style-scope gr-diff br"></span>';
- const WBR_HTML = '<wbr class="style-scope gr-diff">';
-
- setup(() => {
- element = basicFixture.instantiate();
- stubRestApi('getLoggedIn').returns(Promise.resolve(false));
- stubRestApi('getProjectConfig').returns(Promise.resolve({}));
- stubBaseUrl('/r');
- prefs = {...DEFAULT_PREFS};
- builder = new GrDiffBuilderLegacy({content: []}, prefs);
- });
-
- test('line_length applied with <wbr> if line_wrapping is true', () => {
- builder._prefs = {line_wrapping: true, tab_size: 4, line_length: 50};
- const text = 'a'.repeat(51);
-
- const line = {text, highlights: []};
- const expected = 'a'.repeat(50) + WBR_HTML + 'a';
- const result = builder.createTextEl(undefined, line).firstChild.innerHTML;
- assert.equal(result, expected);
- });
-
- test('line_length applied with line break if line_wrapping is false', () => {
- builder._prefs = {line_wrapping: false, tab_size: 4, line_length: 50};
- const text = 'a'.repeat(51);
-
- const line = {text, highlights: []};
- const expected = 'a'.repeat(50) + LINE_BREAK_HTML + 'a';
- const result = builder.createTextEl(undefined, line).firstChild.innerHTML;
- assert.equal(result, expected);
- });
-
- [DiffViewMode.UNIFIED, DiffViewMode.SIDE_BY_SIDE]
- .forEach(mode => {
- test(`line_length used for regular files under ${mode}`, () => {
- element.path = '/a.txt';
- element.viewMode = mode;
- 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;
- element.diff = {};
- element.prefs = {tab_size: 4, line_length: 50};
- builder = element._getDiffBuilder();
- assert.equal(builder._prefs.line_length, 72);
- });
- });
-
- test('createTextEl linewrap with tabs', () => {
- const text = '\t'.repeat(7) + '!';
- const line = {text, highlights: []};
- const el = builder.createTextEl(undefined, line);
- assert.equal(el.innerText, text);
- // With line length 10 and tab size 2, there should be a line break
- // after every two tabs.
- const newlineEl = el.querySelector('.contentText > .br');
- assert.isOk(newlineEl);
- assert.equal(
- el.querySelector('.contentText .tab:nth-child(2)').nextSibling,
- newlineEl);
- });
-
- test('_handlePreferenceError throws with invalid preference', () => {
- element.prefs = {tab_size: 0};
- assert.throws(() => element._getDiffBuilder());
- });
-
- test('_handlePreferenceError triggers alert and javascript error', () => {
- const errorStub = sinon.stub();
- element.addEventListener('show-alert', errorStub);
- assert.throws(() => element._handlePreferenceError('tab size'));
- assert.equal(errorStub.lastCall.args[0].detail.message,
- `The value of the 'tab size' user preference is invalid. ` +
- `Fix in diff preferences`);
- });
-
- suite('intraline differences', () => {
- let el;
- let str;
- let annotateElementSpy;
- let layer;
- const lineNumberEl = document.createElement('td');
-
- function slice(str, start, end) {
- return Array.from(str).slice(start, end)
- .join('');
- }
-
- setup(() => {
- el = divWithTextFixture.instantiate();
- str = el.textContent;
- annotateElementSpy = sinon.spy(GrAnnotation, 'annotateElement');
- layer = document.createElement('gr-diff-builder')
- ._createIntralineLayer();
- });
-
- test('annotate no highlights', () => {
- const line = {
- text: str,
- highlights: [],
- };
-
- layer.annotate(el, lineNumberEl, line);
-
- // The content is unchanged.
- assert.isFalse(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 1);
- assert.instanceOf(el.childNodes[0], Text);
- assert.equal(str, el.childNodes[0].textContent);
- });
-
- test('annotate with highlights', () => {
- const line = {
- text: str,
- highlights: [
- {startIndex: 6, endIndex: 12},
- {startIndex: 18, endIndex: 22},
- ],
- };
- const str0 = slice(str, 0, 6);
- const str1 = slice(str, 6, 12);
- const str2 = slice(str, 12, 18);
- const str3 = slice(str, 18, 22);
- const str4 = slice(str, 22);
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 5);
-
- assert.instanceOf(el.childNodes[0], Text);
- assert.equal(el.childNodes[0].textContent, str0);
-
- assert.notInstanceOf(el.childNodes[1], Text);
- assert.equal(el.childNodes[1].textContent, str1);
-
- assert.instanceOf(el.childNodes[2], Text);
- assert.equal(el.childNodes[2].textContent, str2);
-
- assert.notInstanceOf(el.childNodes[3], Text);
- assert.equal(el.childNodes[3].textContent, str3);
-
- assert.instanceOf(el.childNodes[4], Text);
- assert.equal(el.childNodes[4].textContent, str4);
- });
-
- test('annotate without endIndex', () => {
- const line = {
- text: str,
- highlights: [
- {startIndex: 28},
- ],
- };
-
- const str0 = slice(str, 0, 28);
- const str1 = slice(str, 28);
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 2);
-
- assert.instanceOf(el.childNodes[0], Text);
- assert.equal(el.childNodes[0].textContent, str0);
-
- assert.notInstanceOf(el.childNodes[1], Text);
- assert.equal(el.childNodes[1].textContent, str1);
- });
-
- test('annotate ignores empty highlights', () => {
- const line = {
- text: str,
- highlights: [
- {startIndex: 28, endIndex: 28},
- ],
- };
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 1);
- });
-
- test('annotate handles unicode', () => {
- // Put some unicode into the string:
- str = str.replace(/\s/g, '💢');
- el.textContent = str;
- const line = {
- text: str,
- highlights: [
- {startIndex: 6, endIndex: 12},
- ],
- };
-
- const str0 = slice(str, 0, 6);
- const str1 = slice(str, 6, 12);
- const str2 = slice(str, 12);
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 3);
-
- assert.instanceOf(el.childNodes[0], Text);
- assert.equal(el.childNodes[0].textContent, str0);
-
- assert.notInstanceOf(el.childNodes[1], Text);
- assert.equal(el.childNodes[1].textContent, str1);
-
- assert.instanceOf(el.childNodes[2], Text);
- assert.equal(el.childNodes[2].textContent, str2);
- });
-
- test('annotate handles unicode w/o endIndex', () => {
- // Put some unicode into the string:
- str = str.replace(/\s/g, '💢');
- el.textContent = str;
-
- const line = {
- text: str,
- highlights: [
- {startIndex: 6},
- ],
- };
-
- const str0 = slice(str, 0, 6);
- const str1 = slice(str, 6);
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isTrue(annotateElementSpy.called);
- assert.equal(el.childNodes.length, 2);
-
- assert.instanceOf(el.childNodes[0], Text);
- assert.equal(el.childNodes[0].textContent, str0);
-
- assert.notInstanceOf(el.childNodes[1], Text);
- assert.equal(el.childNodes[1].textContent, str1);
- });
- });
-
- suite('tab indicators', () => {
- let element;
- let layer;
- const lineNumberEl = document.createElement('td');
-
- setup(() => {
- element = basicFixture.instantiate();
- element._showTabs = true;
- layer = element._createTabIndicatorLayer();
- });
-
- test('does nothing with empty line', () => {
- const line = {text: ''};
- const el = document.createElement('div');
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementStub.called);
- });
-
- test('does nothing with no tabs', () => {
- const str = 'lorem ipsum no tabs';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementStub.called);
- });
-
- test('annotates tab at beginning', () => {
- const str = '\tlorem upsum';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.equal(annotateElementStub.callCount, 1);
- const args = annotateElementStub.getCalls()[0].args;
- assert.equal(args[0], el);
- assert.equal(args[1], 0, 'offset of tab indicator');
- assert.equal(args[2], 1, 'length of tab indicator');
- assert.include(args[3], 'tab-indicator');
- });
-
- test('does not annotate when disabled', () => {
- element._showTabs = false;
-
- const str = '\tlorem upsum';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.isFalse(annotateElementStub.called);
- });
-
- test('annotates multiple in beginning', () => {
- const str = '\t\tlorem upsum';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.equal(annotateElementStub.callCount, 2);
-
- let args = annotateElementStub.getCalls()[0].args;
- assert.equal(args[0], el);
- assert.equal(args[1], 0, 'offset of tab indicator');
- assert.equal(args[2], 1, 'length of tab indicator');
- assert.include(args[3], 'tab-indicator');
-
- args = annotateElementStub.getCalls()[1].args;
- assert.equal(args[0], el);
- assert.equal(args[1], 1, 'offset of tab indicator');
- assert.equal(args[2], 1, 'length of tab indicator');
- assert.include(args[3], 'tab-indicator');
- });
-
- test('annotates intermediate tabs', () => {
- const str = 'lorem\tupsum';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
-
- layer.annotate(el, lineNumberEl, line);
-
- assert.equal(annotateElementStub.callCount, 1);
- const args = annotateElementStub.getCalls()[0].args;
- assert.equal(args[0], el);
- assert.equal(args[1], 5, 'offset of tab indicator');
- assert.equal(args[2], 1, 'length of tab indicator');
- assert.include(args[3], 'tab-indicator');
- });
- });
-
- suite('layers', () => {
- let element;
- let initialLayersCount;
- let withLayerCount;
- setup(() => {
- const layers = [];
- element = basicFixture.instantiate();
- element.layers = layers;
- element._showTrailingWhitespace = true;
- element._setupAnnotationLayers();
- initialLayersCount = element._layers.length;
- });
-
- test('no layers', () => {
- element._setupAnnotationLayers();
- assert.equal(element._layers.length, initialLayersCount);
- });
-
- suite('with layers', () => {
- const layers = [{}, {}];
- setup(() => {
- element = basicFixture.instantiate();
- element.layers = layers;
- element._showTrailingWhitespace = true;
- element._setupAnnotationLayers();
- withLayerCount = element._layers.length;
- });
- test('with layers', () => {
- element._setupAnnotationLayers();
- assert.equal(element._layers.length, withLayerCount);
- assert.equal(initialLayersCount + layers.length,
- withLayerCount);
- });
- });
- });
-
- suite('trailing whitespace', () => {
- let element;
- let layer;
- const lineNumberEl = document.createElement('td');
-
- setup(() => {
- element = basicFixture.instantiate();
- element._showTrailingWhitespace = true;
- layer = element._createTrailingWhitespaceLayer();
- });
-
- test('does nothing with empty line', () => {
- const line = {text: ''};
- const el = document.createElement('div');
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isFalse(annotateElementStub.called);
- });
-
- test('does nothing with no trailing whitespace', () => {
- const str = 'lorem ipsum blah blah';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isFalse(annotateElementStub.called);
- });
-
- test('annotates trailing spaces', () => {
- const str = 'lorem ipsum ';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isTrue(annotateElementStub.called);
- assert.equal(annotateElementStub.lastCall.args[1], 11);
- assert.equal(annotateElementStub.lastCall.args[2], 3);
- });
-
- test('annotates trailing tabs', () => {
- const str = 'lorem ipsum\t\t\t';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isTrue(annotateElementStub.called);
- assert.equal(annotateElementStub.lastCall.args[1], 11);
- assert.equal(annotateElementStub.lastCall.args[2], 3);
- });
-
- test('annotates mixed trailing whitespace', () => {
- const str = 'lorem ipsum\t \t';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isTrue(annotateElementStub.called);
- assert.equal(annotateElementStub.lastCall.args[1], 11);
- assert.equal(annotateElementStub.lastCall.args[2], 3);
- });
-
- test('unicode preceding trailing whitespace', () => {
- const str = '💢\t';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isTrue(annotateElementStub.called);
- assert.equal(annotateElementStub.lastCall.args[1], 1);
- assert.equal(annotateElementStub.lastCall.args[2], 1);
- });
-
- test('does not annotate when disabled', () => {
- element._showTrailingWhitespace = false;
- const str = 'lorem upsum\t \t ';
- const line = {text: str};
- const el = document.createElement('div');
- el.textContent = str;
- const annotateElementStub =
- sinon.stub(GrAnnotation, 'annotateElement');
- layer.annotate(el, lineNumberEl, line);
- assert.isFalse(annotateElementStub.called);
- });
- });
-
- suite('rendering text, images and binary files', () => {
- let processStub;
- let keyLocations;
- let content;
-
- setup(() => {
- element = basicFixture.instantiate();
- element.viewMode = 'SIDE_BY_SIDE';
- processStub = sinon.stub(element.processor, 'process')
- .returns(Promise.resolve());
- keyLocations = {left: {}, right: {}};
- element.prefs = {
- ...DEFAULT_PREFS,
- context: -1,
- syntax_highlighting: true,
- };
- content = [{
- a: ['all work and no play make andybons a dull boy'],
- b: ['elgoog elgoog elgoog'],
- }, {
- ab: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
- ],
- }];
- });
-
- test('text', async () => {
- element.diff = {content};
- element.render(keyLocations);
- await waitForEventOnce(element, 'render-content');
- assert.isTrue(processStub.calledOnce);
- assert.isFalse(processStub.lastCall.args[1]);
- });
-
- test('image', async () => {
- element.diff = {content, binary: true};
- element.isImageDiff = true;
- element.render(keyLocations);
- await waitForEventOnce(element, 'render-content');
- assert.isTrue(processStub.calledOnce);
- assert.isTrue(processStub.lastCall.args[1]);
- });
-
- test('binary', async () => {
- element.diff = {content, binary: true};
- element.render(keyLocations);
- await waitForEventOnce(element, 'render-content');
- assert.isTrue(processStub.calledOnce);
- assert.isTrue(processStub.lastCall.args[1]);
- });
- });
-
- suite('rendering', () => {
- let content;
- let outputEl;
- let keyLocations;
-
- setup(async () => {
- const prefs = {...DEFAULT_PREFS};
- content = [
- {
- a: ['all work and no play make andybons a dull boy'],
- b: ['elgoog elgoog elgoog'],
- },
- {
- ab: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
- ],
- },
- ];
- element = basicFixture.instantiate();
- sinon.stub(element, 'dispatchEvent');
- outputEl = element.querySelector('#diffTable');
- keyLocations = {left: {}, right: {}};
- sinon.stub(element, '_getDiffBuilder').callsFake(() => {
- const builder = new GrDiffBuilderSideBySide({content}, prefs, outputEl);
- sinon.stub(builder, 'addColumns');
- builder.buildSectionElement = function(group) {
- const section = document.createElement('stub');
- section.textContent = group.lines
- .reduce((acc, line) => acc + line.text, '');
- return section;
- };
- return builder;
- });
- element.diff = {content};
- element.prefs = prefs;
- await element.render(keyLocations);
- });
-
- test('addColumns is called', () => {
- assert.isTrue(element._builder.addColumns.called);
- });
-
- test('getGroupsByLineRange one line', () => {
- const section = outputEl.querySelector('stub:nth-of-type(3)');
- const groups = element._builder.getGroupsByLineRange(1, 1, 'left');
- assert.equal(groups.length, 1);
- assert.strictEqual(groups[0].element, section);
- });
-
- test('getGroupsByLineRange over diff', () => {
- const section = [
- outputEl.querySelector('stub:nth-of-type(3)'),
- outputEl.querySelector('stub:nth-of-type(4)'),
- ];
- 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 () => {
- await new Promise(resolve => afterNextRender(element, resolve));
- const firedEventTypes = element.dispatchEvent.getCalls()
- .map(c => c.args[0].type);
- assert.include(firedEventTypes, 'render-start');
- assert.include(firedEventTypes, 'render-content');
- });
-
- 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();
- sinon.stub(element, 'dispatchEvent');
- 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', async () => {
- element.dispatchEvent.reset();
- 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');
-
- await new Promise(resolve => afterNextRender(element, resolve));
- const firedEventTypes = element.dispatchEvent.getCalls()
- .map(c => c.args[0].type);
- assert.include(firedEventTypes, 'render-content');
- });
- });
-
- suite('mock-diff', () => {
- let element;
- let builder;
- let diff;
- let keyLocations;
-
- setup(async () => {
- element = mockDiffFixture.instantiate();
- diff = createDiff();
- element.diff = diff;
-
- keyLocations = {left: {}, right: {}};
-
- element.prefs = {
- line_length: 80,
- show_tabs: true,
- tab_size: 4,
- };
- await element.render(keyLocations);
- builder = element._builder;
- });
-
- test('aria-labels on added line numbers', () => {
- const deltaLineNumberButton = element.diffElement.querySelectorAll(
- '.lineNumButton.right')[5];
-
- assert.isOk(deltaLineNumberButton);
- assert.equal(deltaLineNumberButton.getAttribute('aria-label'), '5 added');
- });
-
- test('aria-labels on removed line numbers', () => {
- const deltaLineNumberButton = element.diffElement.querySelectorAll(
- '.lineNumButton.left')[10];
-
- assert.isOk(deltaLineNumberButton);
- assert.equal(
- deltaLineNumberButton.getAttribute('aria-label'), '10 removed');
- });
-
- test('getContentByLine', () => {
- let actual;
-
- actual = builder.getContentByLine(2, 'left');
- assert.equal(actual.textContent, diff.content[0].ab[1]);
-
- actual = builder.getContentByLine(2, 'right');
- assert.equal(actual.textContent, diff.content[0].ab[1]);
-
- actual = builder.getContentByLine(5, 'left');
- assert.equal(actual.textContent, diff.content[2].ab[0]);
-
- actual = builder.getContentByLine(5, 'right');
- assert.equal(actual.textContent, diff.content[1].b[0]);
- });
-
- test('getContentTdByLineEl works both with button and td', () => {
- const diffRow = element.diffElement.querySelectorAll('tr.diff-row')[2];
-
- const lineNumTdLeft = diffRow.querySelector('td.lineNum.left');
- const lineNumButtonLeft = lineNumTdLeft.querySelector('button');
- const contentTdLeft = diffRow.querySelectorAll('.content')[0];
-
- const lineNumTdRight = diffRow.querySelector('td.lineNum.right');
- const lineNumButtonRight = lineNumTdRight.querySelector('button');
- const contentTdRight = diffRow.querySelectorAll('.content')[1];
-
- assert.equal(element.getContentTdByLineEl(lineNumTdLeft), contentTdLeft);
- assert.equal(
- element.getContentTdByLineEl(lineNumButtonLeft), contentTdLeft);
- assert.equal(
- element.getContentTdByLineEl(lineNumTdRight), contentTdRight);
- assert.equal(
- element.getContentTdByLineEl(lineNumButtonRight), contentTdRight);
- });
-
- test('findLinesByRange', () => {
- const lines = [];
- const elems = [];
- const start = 6;
- const end = 10;
- const count = end - start + 1;
-
- builder.findLinesByRange(start, end, 'right', lines, elems);
-
- assert.equal(lines.length, count);
- assert.equal(elems.length, count);
-
- for (let i = 0; i < 5; i++) {
- assert.instanceOf(lines[i], GrDiffLine);
- assert.equal(lines[i].afterNumber, start + i);
- assert.instanceOf(elems[i], HTMLElement);
- assert.equal(lines[i].text, elems[i].textContent);
- }
- });
-
- test('renderContentByRange', () => {
- const spy = sinon.spy(builder, 'createTextEl');
- const start = 9;
- const end = 14;
- const count = end - start + 1;
-
- builder.renderContentByRange(start, end, 'left');
-
- assert.equal(spy.callCount, count);
- spy.getCalls().forEach((call, i) => {
- assert.equal(call.args[1].beforeNumber, start + i);
- });
- });
-
- test('renderContentByRange non-existent elements', () => {
- const spy = sinon.spy(builder, 'createTextEl');
-
- sinon.stub(builder, 'getLineNumberEl').returns(
- document.createElement('div')
- );
- sinon.stub(builder, 'findLinesByRange').callsFake(
- (s, e, d, lines, elements) => {
- // Add a line and a corresponding element.
- lines.push(new GrDiffLine(GrDiffLineType.BOTH));
- const tr = document.createElement('tr');
- const td = document.createElement('td');
- const el = document.createElement('div');
- tr.appendChild(td);
- td.appendChild(el);
- elements.push(el);
-
- // Add 2 lines without corresponding elements.
- lines.push(new GrDiffLine(GrDiffLineType.BOTH));
- lines.push(new GrDiffLine(GrDiffLineType.BOTH));
- });
-
- builder.renderContentByRange(1, 10, 'left');
- // Should be called only once because only one line had a corresponding
- // element.
- assert.equal(spy.callCount, 1);
- });
-
- test('getLineNumberEl side-by-side left', () => {
- const contentEl = builder.getContentByLine(5, 'left',
- element.$.diffTable);
- const lineNumberEl = builder.getLineNumberEl(contentEl, 'left');
- assert.isTrue(lineNumberEl.classList.contains('lineNum'));
- assert.isTrue(lineNumberEl.classList.contains('left'));
- });
-
- test('getLineNumberEl side-by-side right', () => {
- const contentEl = builder.getContentByLine(5, 'right',
- element.$.diffTable);
- const lineNumberEl = builder.getLineNumberEl(contentEl, 'right');
- assert.isTrue(lineNumberEl.classList.contains('lineNum'));
- assert.isTrue(lineNumberEl.classList.contains('right'));
- });
-
- test('getLineNumberEl unified left', async () => {
- // Re-render as unified:
- element.viewMode = 'UNIFIED_DIFF';
- await element.render(keyLocations);
- builder = element._builder;
-
- const contentEl = builder.getContentByLine(5, 'left',
- element.$.diffTable);
- const lineNumberEl = builder.getLineNumberEl(contentEl, 'left');
- assert.isTrue(lineNumberEl.classList.contains('lineNum'));
- assert.isTrue(lineNumberEl.classList.contains('left'));
- });
-
- test('getLineNumberEl unified right', async () => {
- // Re-render as unified:
- element.viewMode = 'UNIFIED_DIFF';
- await element.render(keyLocations);
- builder = element._builder;
-
- const contentEl = builder.getContentByLine(5, 'right',
- element.$.diffTable);
- const lineNumberEl = builder.getLineNumberEl(contentEl, 'right');
- assert.isTrue(lineNumberEl.classList.contains('lineNum'));
- assert.isTrue(lineNumberEl.classList.contains('right'));
- });
-
- test('getNextContentOnSide side-by-side left', () => {
- const startElem = builder.getContentByLine(5, 'left',
- element.$.diffTable);
- const expectedStartString = diff.content[2].ab[0];
- const expectedNextString = diff.content[2].ab[1];
- assert.equal(startElem.textContent, expectedStartString);
-
- const nextElem = builder.getNextContentOnSide(startElem,
- 'left');
- assert.equal(nextElem.textContent, expectedNextString);
- });
-
- test('getNextContentOnSide side-by-side right', () => {
- const startElem = builder.getContentByLine(5, 'right',
- element.$.diffTable);
- const expectedStartString = diff.content[1].b[0];
- const expectedNextString = diff.content[1].b[1];
- assert.equal(startElem.textContent, expectedStartString);
-
- const nextElem = builder.getNextContentOnSide(startElem,
- 'right');
- assert.equal(nextElem.textContent, expectedNextString);
- });
-
- test('getNextContentOnSide unified left', async () => {
- // Re-render as unified:
- element.viewMode = 'UNIFIED_DIFF';
- await element.render(keyLocations);
- builder = element._builder;
-
- const startElem = builder.getContentByLine(5, 'left',
- element.$.diffTable);
- const expectedStartString = diff.content[2].ab[0];
- const expectedNextString = diff.content[2].ab[1];
- assert.equal(startElem.textContent, expectedStartString);
-
- const nextElem = builder.getNextContentOnSide(startElem,
- 'left');
- assert.equal(nextElem.textContent, expectedNextString);
- });
-
- test('getNextContentOnSide unified right', async () => {
- // Re-render as unified:
- element.viewMode = 'UNIFIED_DIFF';
- await element.render(keyLocations);
- builder = element._builder;
-
- const startElem = builder.getContentByLine(5, 'right',
- element.$.diffTable);
- const expectedStartString = diff.content[1].b[0];
- const expectedNextString = diff.content[1].b[1];
- assert.equal(startElem.textContent, expectedStartString);
-
- const nextElem = builder.getNextContentOnSide(startElem,
- 'right');
- assert.equal(nextElem.textContent, expectedNextString);
- });
- });
-
- suite('blame', () => {
- let mockBlame;
-
- setup(() => {
- mockBlame = [
- {id: 'commit 1', ranges: [{start: 1, end: 2}, {start: 10, end: 16}]},
- {id: 'commit 2', ranges: [{start: 4, end: 10}, {start: 17, end: 32}]},
- ];
- });
-
- test('setBlame attempts to render each blamed line', () => {
- const getBlameStub = sinon.stub(builder, 'getBlameTdByLine')
- .returns(null);
- builder.setBlame(mockBlame);
- assert.equal(getBlameStub.callCount, 32);
- });
-
- test('getBlameCommitForBaseLine', () => {
- sinon.stub(builder, 'getBlameTdByLine').returns(undefined);
- builder.setBlame(mockBlame);
- assert.isOk(builder.getBlameCommitForBaseLine(1));
- assert.equal(builder.getBlameCommitForBaseLine(1).id, 'commit 1');
-
- assert.isOk(builder.getBlameCommitForBaseLine(11));
- assert.equal(builder.getBlameCommitForBaseLine(11).id, 'commit 1');
-
- assert.isOk(builder.getBlameCommitForBaseLine(32));
- assert.equal(builder.getBlameCommitForBaseLine(32).id, 'commit 2');
-
- assert.isUndefined(builder.getBlameCommitForBaseLine(33));
- });
-
- test('getBlameCommitForBaseLine w/o blame returns null', () => {
- assert.isUndefined(builder.getBlameCommitForBaseLine(1));
- assert.isUndefined(builder.getBlameCommitForBaseLine(11));
- assert.isUndefined(builder.getBlameCommitForBaseLine(31));
- });
-
- test('createBlameCell', () => {
- const mockBlameInfo = {
- time: 1576155200,
- id: 1234567890,
- author: 'Clark Kent',
- commit_msg: 'Testing Commit',
- ranges: [1],
- };
- const getBlameStub = sinon.stub(builder, 'getBlameCommitForBaseLine')
- .returns(mockBlameInfo);
- const line = new GrDiffLine(GrDiffLineType.BOTH);
- line.beforeNumber = 3;
- line.afterNumber = 5;
-
- const result = builder.createBlameCell(line.beforeNumber);
-
- assert.isTrue(getBlameStub.calledWithExactly(3));
- assert.equal(result.getAttribute('data-line-number'), '3');
- expect(result).dom.to.equal(/* HTML */`
- <span class="gr-diff style-scope">
- <a class="blameDate gr-diff style-scope" href="/r/q/1234567890">
- 12/12/2019
- </a>
- <span class="blameAuthor gr-diff style-scope">Clark</span>
- <gr-hovercard class="gr-diff style-scope">
- <span class="blameHoverCard gr-diff style-scope">
- Commit 1234567890<br>
- Author: Clark Kent<br>
- Date: 12/12/2019<br>
- <br>
- Testing Commit
- </span>
- </gr-hovercard>
- </span>
- `);
- });
- });
-});
-
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts
new file mode 100644
index 0000000..d37afc0
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-element_test.ts
@@ -0,0 +1,1127 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import {
+ createConfig,
+ createDiff,
+ createEmptyDiff,
+} from '../../../test/test-data-generators';
+import './gr-diff-builder-element';
+import {queryAndAssert, stubBaseUrl, waitUntil} from '../../../test/test-utils';
+import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
+import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line';
+import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side';
+import {
+ DiffContent,
+ DiffInfo,
+ DiffLayer,
+ DiffPreferencesInfo,
+ DiffViewMode,
+ Side,
+} from '../../../api/diff';
+import {stubRestApi} from '../../../test/test-utils';
+import {GrDiffBuilderLegacy} from './gr-diff-builder-legacy';
+import {waitForEventOnce} from '../../../utils/event-util';
+import {GrDiffBuilderElement} from './gr-diff-builder-element';
+import {createDefaultDiffPrefs} from '../../../constants/constants';
+import {KeyLocations} from '../gr-diff-processor/gr-diff-processor';
+import {BlameInfo} from '../../../types/common';
+import {fixture, html} from '@open-wc/testing-helpers';
+
+const DEFAULT_PREFS = createDefaultDiffPrefs();
+
+suite('gr-diff-builder tests', () => {
+ let element: GrDiffBuilderElement;
+ let builder: GrDiffBuilderLegacy;
+ let diffTable: HTMLTableElement;
+
+ const LINE_BREAK_HTML = '<span class="style-scope gr-diff br"></span>';
+ const WBR_HTML = '<wbr class="style-scope gr-diff">';
+
+ const setBuilderPrefs = (prefs: Partial<DiffPreferencesInfo>) => {
+ builder = new GrDiffBuilderSideBySide(
+ createEmptyDiff(),
+ {...createDefaultDiffPrefs(), ...prefs},
+ diffTable
+ );
+ };
+
+ const line = (text: string) => {
+ const line = new GrDiffLine(GrDiffLineType.BOTH);
+ line.text = text;
+ return line;
+ };
+
+ setup(async () => {
+ diffTable = await fixture(html`<table id="diffTable"></table>`);
+ element = new GrDiffBuilderElement();
+ element.diffElement = diffTable;
+ stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+ stubRestApi('getProjectConfig').returns(Promise.resolve(createConfig()));
+ stubBaseUrl('/r');
+ setBuilderPrefs({});
+ });
+
+ test('line_length applied with <wbr> if line_wrapping is true', () => {
+ setBuilderPrefs({line_wrapping: true, tab_size: 4, line_length: 50});
+ const text = 'a'.repeat(51);
+ const expected = 'a'.repeat(50) + WBR_HTML + 'a';
+ const result = builder.createTextEl(null, line(text)).firstElementChild
+ ?.innerHTML;
+ assert.equal(result, expected);
+ });
+
+ test('line_length applied with line break if line_wrapping is false', () => {
+ setBuilderPrefs({line_wrapping: false, tab_size: 4, line_length: 50});
+ const text = 'a'.repeat(51);
+ const expected = 'a'.repeat(50) + LINE_BREAK_HTML + 'a';
+ const result = builder.createTextEl(null, line(text)).firstElementChild
+ ?.innerHTML;
+ assert.equal(result, expected);
+ });
+
+ [DiffViewMode.UNIFIED, DiffViewMode.SIDE_BY_SIDE].forEach(mode => {
+ test(`line_length used for regular files under ${mode}`, () => {
+ element.path = '/a.txt';
+ element.viewMode = mode;
+ element.diff = createEmptyDiff();
+ element.prefs = {
+ ...createDefaultDiffPrefs(),
+ tab_size: 4,
+ line_length: 50,
+ };
+ builder = element.getDiffBuilder() as GrDiffBuilderLegacy;
+ assert.equal(builder._prefs.line_length, 50);
+ });
+
+ test(`line_length ignored for commit msg under ${mode}`, () => {
+ element.path = '/COMMIT_MSG';
+ element.viewMode = mode;
+ element.diff = createEmptyDiff();
+ element.prefs = {
+ ...createDefaultDiffPrefs(),
+ tab_size: 4,
+ line_length: 50,
+ };
+ builder = element.getDiffBuilder() as GrDiffBuilderLegacy;
+ assert.equal(builder._prefs.line_length, 72);
+ });
+ });
+
+ test('createTextEl linewrap with tabs', () => {
+ setBuilderPrefs({tab_size: 4, line_length: 10});
+ const text = '\t'.repeat(7) + '!';
+ const el = builder.createTextEl(null, line(text));
+ assert.equal(el.innerText, text);
+ // With line length 10 and tab size 4, there should be a line break
+ // after every two tabs.
+ const newlineEl = el.querySelector('.contentText > .br');
+ assert.isOk(newlineEl);
+ assert.equal(
+ el.querySelector('.contentText .tab:nth-child(2)')?.nextSibling,
+ newlineEl
+ );
+ });
+
+ test('_handlePreferenceError throws with invalid preference', () => {
+ element.prefs = {...createDefaultDiffPrefs(), tab_size: 0};
+ assert.throws(() => element.getDiffBuilder());
+ });
+
+ test('_handlePreferenceError triggers alert and javascript error', () => {
+ const errorStub = sinon.stub();
+ diffTable.addEventListener('show-alert', errorStub);
+ assert.throws(() => element.handlePreferenceError('tab size'));
+ assert.equal(
+ errorStub.lastCall.args[0].detail.message,
+ "The value of the 'tab size' user preference is invalid. " +
+ 'Fix in diff preferences'
+ );
+ });
+
+ suite('intraline differences', () => {
+ let el: HTMLElement;
+ let str: string;
+ let annotateElementSpy: sinon.SinonSpy;
+ let layer: DiffLayer;
+ const lineNumberEl = document.createElement('td');
+
+ function slice(str: string, start: number, end?: number) {
+ return Array.from(str).slice(start, end).join('');
+ }
+
+ setup(async () => {
+ el = await fixture(html`
+ <div>Lorem ipsum dolor sit amet, suspendisse inceptos vehicula</div>
+ `);
+ str = el.textContent ?? '';
+ annotateElementSpy = sinon.spy(GrAnnotation, 'annotateElement');
+ layer = element.createIntralineLayer();
+ });
+
+ test('annotate no highlights', () => {
+ layer.annotate(el, lineNumberEl, line(str), Side.LEFT);
+
+ // The content is unchanged.
+ assert.isFalse(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 1);
+ assert.instanceOf(el.childNodes[0], Text);
+ assert.equal(str, el.childNodes[0].textContent);
+ });
+
+ test('annotate with highlights', () => {
+ const l = line(str);
+ l.highlights = [
+ {contentIndex: 0, startIndex: 6, endIndex: 12},
+ {contentIndex: 0, startIndex: 18, endIndex: 22},
+ ];
+ const str0 = slice(str, 0, 6);
+ const str1 = slice(str, 6, 12);
+ const str2 = slice(str, 12, 18);
+ const str3 = slice(str, 18, 22);
+ const str4 = slice(str, 22);
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.isTrue(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 5);
+
+ assert.instanceOf(el.childNodes[0], Text);
+ assert.equal(el.childNodes[0].textContent, str0);
+
+ assert.notInstanceOf(el.childNodes[1], Text);
+ assert.equal(el.childNodes[1].textContent, str1);
+
+ assert.instanceOf(el.childNodes[2], Text);
+ assert.equal(el.childNodes[2].textContent, str2);
+
+ assert.notInstanceOf(el.childNodes[3], Text);
+ assert.equal(el.childNodes[3].textContent, str3);
+
+ assert.instanceOf(el.childNodes[4], Text);
+ assert.equal(el.childNodes[4].textContent, str4);
+ });
+
+ test('annotate without endIndex', () => {
+ const l = line(str);
+ l.highlights = [{contentIndex: 0, startIndex: 28}];
+
+ const str0 = slice(str, 0, 28);
+ const str1 = slice(str, 28);
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.isTrue(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 2);
+
+ assert.instanceOf(el.childNodes[0], Text);
+ assert.equal(el.childNodes[0].textContent, str0);
+
+ assert.notInstanceOf(el.childNodes[1], Text);
+ assert.equal(el.childNodes[1].textContent, str1);
+ });
+
+ test('annotate ignores empty highlights', () => {
+ const l = line(str);
+ l.highlights = [{contentIndex: 0, startIndex: 28, endIndex: 28}];
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.isFalse(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 1);
+ });
+
+ test('annotate handles unicode', () => {
+ // Put some unicode into the string:
+ str = str.replace(/\s/g, '💢');
+ el.textContent = str;
+ const l = line(str);
+ l.highlights = [{contentIndex: 0, startIndex: 6, endIndex: 12}];
+
+ const str0 = slice(str, 0, 6);
+ const str1 = slice(str, 6, 12);
+ const str2 = slice(str, 12);
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.isTrue(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 3);
+
+ assert.instanceOf(el.childNodes[0], Text);
+ assert.equal(el.childNodes[0].textContent, str0);
+
+ assert.notInstanceOf(el.childNodes[1], Text);
+ assert.equal(el.childNodes[1].textContent, str1);
+
+ assert.instanceOf(el.childNodes[2], Text);
+ assert.equal(el.childNodes[2].textContent, str2);
+ });
+
+ test('annotate handles unicode w/o endIndex', () => {
+ // Put some unicode into the string:
+ str = str.replace(/\s/g, '💢');
+ el.textContent = str;
+
+ const l = line(str);
+ l.highlights = [{contentIndex: 0, startIndex: 6}];
+
+ const str0 = slice(str, 0, 6);
+ const str1 = slice(str, 6);
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.isTrue(annotateElementSpy.called);
+ assert.equal(el.childNodes.length, 2);
+
+ assert.instanceOf(el.childNodes[0], Text);
+ assert.equal(el.childNodes[0].textContent, str0);
+
+ assert.notInstanceOf(el.childNodes[1], Text);
+ assert.equal(el.childNodes[1].textContent, str1);
+ });
+ });
+
+ suite('tab indicators', () => {
+ let layer: DiffLayer;
+ const lineNumberEl = document.createElement('td');
+
+ setup(() => {
+ element.showTabs = true;
+ layer = element.createTabIndicatorLayer();
+ });
+
+ test('does nothing with empty line', () => {
+ const l = line('');
+ const el = document.createElement('div');
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('does nothing with no tabs', () => {
+ const str = 'lorem ipsum no tabs';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('annotates tab at beginning', () => {
+ const str = '\tlorem upsum';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.equal(annotateElementStub.callCount, 1);
+ const args = annotateElementStub.getCalls()[0].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], 0, 'offset of tab indicator');
+ assert.equal(args[2], 1, 'length of tab indicator');
+ assert.include(args[3], 'tab-indicator');
+ });
+
+ test('does not annotate when disabled', () => {
+ element.showTabs = false;
+
+ const str = '\tlorem upsum';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('annotates multiple in beginning', () => {
+ const str = '\t\tlorem upsum';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.equal(annotateElementStub.callCount, 2);
+
+ let args = annotateElementStub.getCalls()[0].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], 0, 'offset of tab indicator');
+ assert.equal(args[2], 1, 'length of tab indicator');
+ assert.include(args[3], 'tab-indicator');
+
+ args = annotateElementStub.getCalls()[1].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], 1, 'offset of tab indicator');
+ assert.equal(args[2], 1, 'length of tab indicator');
+ assert.include(args[3], 'tab-indicator');
+ });
+
+ test('annotates intermediate tabs', () => {
+ const str = 'lorem\tupsum';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+
+ assert.equal(annotateElementStub.callCount, 1);
+ const args = annotateElementStub.getCalls()[0].args;
+ assert.equal(args[0], el);
+ assert.equal(args[1], 5, 'offset of tab indicator');
+ assert.equal(args[2], 1, 'length of tab indicator');
+ assert.include(args[3], 'tab-indicator');
+ });
+ });
+
+ suite('layers', () => {
+ let initialLayersCount = 0;
+ let withLayerCount = 0;
+ setup(() => {
+ const layers: DiffLayer[] = [];
+ element.layers = layers;
+ element.showTrailingWhitespace = true;
+ element.setupAnnotationLayers();
+ initialLayersCount = element.layersInternal.length;
+ });
+
+ test('no layers', () => {
+ element.setupAnnotationLayers();
+ assert.equal(element.layersInternal.length, initialLayersCount);
+ });
+
+ suite('with layers', () => {
+ const layers: DiffLayer[] = [{annotate: () => {}}, {annotate: () => {}}];
+ setup(() => {
+ element.layers = layers;
+ element.showTrailingWhitespace = true;
+ element.setupAnnotationLayers();
+ withLayerCount = element.layersInternal.length;
+ });
+ test('with layers', () => {
+ element.setupAnnotationLayers();
+ assert.equal(element.layersInternal.length, withLayerCount);
+ assert.equal(initialLayersCount + layers.length, withLayerCount);
+ });
+ });
+ });
+
+ suite('trailing whitespace', () => {
+ let layer: DiffLayer;
+ const lineNumberEl = document.createElement('td');
+
+ setup(() => {
+ element.showTrailingWhitespace = true;
+ layer = element.createTrailingWhitespaceLayer();
+ });
+
+ test('does nothing with empty line', () => {
+ const l = line('');
+ const el = document.createElement('div');
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('does nothing with no trailing whitespace', () => {
+ const str = 'lorem ipsum blah blah';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ assert.isFalse(annotateElementStub.called);
+ });
+
+ test('annotates trailing spaces', () => {
+ const str = 'lorem ipsum ';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ assert.isTrue(annotateElementStub.called);
+ assert.equal(annotateElementStub.lastCall.args[1], 11);
+ assert.equal(annotateElementStub.lastCall.args[2], 3);
+ });
+
+ test('annotates trailing tabs', () => {
+ const str = 'lorem ipsum\t\t\t';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ assert.isTrue(annotateElementStub.called);
+ assert.equal(annotateElementStub.lastCall.args[1], 11);
+ assert.equal(annotateElementStub.lastCall.args[2], 3);
+ });
+
+ test('annotates mixed trailing whitespace', () => {
+ const str = 'lorem ipsum\t \t';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ assert.isTrue(annotateElementStub.called);
+ assert.equal(annotateElementStub.lastCall.args[1], 11);
+ assert.equal(annotateElementStub.lastCall.args[2], 3);
+ });
+
+ test('unicode preceding trailing whitespace', () => {
+ const str = '💢\t';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ assert.isTrue(annotateElementStub.called);
+ assert.equal(annotateElementStub.lastCall.args[1], 1);
+ assert.equal(annotateElementStub.lastCall.args[2], 1);
+ });
+
+ test('does not annotate when disabled', () => {
+ element.showTrailingWhitespace = false;
+ const str = 'lorem upsum\t \t ';
+ const l = line(str);
+ const el = document.createElement('div');
+ el.textContent = str;
+ const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+ layer.annotate(el, lineNumberEl, l, Side.LEFT);
+ assert.isFalse(annotateElementStub.called);
+ });
+ });
+
+ suite('rendering text, images and binary files', () => {
+ let processStub: sinon.SinonStub;
+ let keyLocations: KeyLocations;
+ let content: DiffContent[] = [];
+
+ setup(() => {
+ element.viewMode = 'SIDE_BY_SIDE';
+ processStub = sinon
+ .stub(element.processor, 'process')
+ .returns(Promise.resolve());
+ keyLocations = {left: {}, right: {}};
+ element.prefs = {
+ ...DEFAULT_PREFS,
+ context: -1,
+ syntax_highlighting: true,
+ };
+ content = [
+ {
+ a: ['all work and no play make andybons a dull boy'],
+ b: ['elgoog elgoog elgoog'],
+ },
+ {
+ ab: [
+ 'Non eram nescius, Brute, cum, quae summis ingeniis ',
+ 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+ ],
+ },
+ ];
+ });
+
+ test('text', async () => {
+ element.diff = {...createEmptyDiff(), content};
+ element.render(keyLocations);
+ await waitForEventOnce(diffTable, 'render-content');
+ assert.isTrue(processStub.calledOnce);
+ assert.isFalse(processStub.lastCall.args[1]);
+ });
+
+ test('image', async () => {
+ element.diff = {...createEmptyDiff(), content, binary: true};
+ element.isImageDiff = true;
+ element.render(keyLocations);
+ await waitForEventOnce(diffTable, 'render-content');
+ assert.isTrue(processStub.calledOnce);
+ assert.isTrue(processStub.lastCall.args[1]);
+ });
+
+ test('binary', async () => {
+ element.diff = {...createEmptyDiff(), content, binary: true};
+ element.render(keyLocations);
+ await waitForEventOnce(diffTable, 'render-content');
+ assert.isTrue(processStub.calledOnce);
+ assert.isTrue(processStub.lastCall.args[1]);
+ });
+ });
+
+ suite('rendering', () => {
+ let content: DiffContent[];
+ let outputEl: HTMLTableElement;
+ let keyLocations: KeyLocations;
+ let addColumnsStub: sinon.SinonStub;
+ let dispatchStub: sinon.SinonStub;
+ let builder: GrDiffBuilderSideBySide;
+
+ setup(() => {
+ const prefs = {...DEFAULT_PREFS};
+ content = [
+ {
+ a: ['all work and no play make andybons a dull boy'],
+ b: ['elgoog elgoog elgoog'],
+ },
+ {
+ ab: [
+ 'Non eram nescius, Brute, cum, quae summis ingeniis ',
+ 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+ ],
+ },
+ ];
+ dispatchStub = sinon.stub(diffTable, 'dispatchEvent');
+ outputEl = element.diffElement!;
+ keyLocations = {left: {}, right: {}};
+ sinon.stub(element, 'getDiffBuilder').callsFake(() => {
+ builder = new GrDiffBuilderSideBySide(
+ {...createEmptyDiff(), content},
+ prefs,
+ outputEl
+ );
+ addColumnsStub = sinon.stub(builder, 'addColumns');
+ builder.buildSectionElement = function (group) {
+ const section = document.createElement('stub');
+ section.style.display = 'block';
+ section.textContent = group.lines.reduce(
+ (acc, line) => acc + line.text,
+ ''
+ );
+ return section;
+ };
+ return builder;
+ });
+ element.diff = {...createEmptyDiff(), content};
+ element.prefs = prefs;
+ element.render(keyLocations);
+ });
+
+ test('addColumns is called', () => {
+ assert.isTrue(addColumnsStub.called);
+ });
+
+ test('getGroupsByLineRange one line', () => {
+ const section = outputEl.querySelector<HTMLElement>(
+ 'stub:nth-of-type(3)'
+ );
+ const groups = builder.getGroupsByLineRange(1, 1, Side.LEFT);
+ assert.equal(groups.length, 1);
+ assert.strictEqual(groups[0].element, section);
+ });
+
+ test('getGroupsByLineRange over diff', () => {
+ const section = [
+ outputEl.querySelector<HTMLElement>('stub:nth-of-type(3)'),
+ outputEl.querySelector<HTMLElement>('stub:nth-of-type(4)'),
+ ];
+ const groups = builder.getGroupsByLineRange(1, 2, Side.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 () => {
+ await waitUntil(() => dispatchStub.callCount >= 1);
+ let firedEventTypes = dispatchStub.getCalls().map(c => c.args[0].type);
+ assert.include(firedEventTypes, 'render-start');
+
+ await waitUntil(() => dispatchStub.callCount >= 2);
+ firedEventTypes = dispatchStub.getCalls().map(c => c.args[0].type);
+ assert.include(firedEventTypes, 'render-content');
+ });
+
+ test('cancel cancels the processor', () => {
+ const processorCancelStub = sinon.stub(element.processor, 'cancel');
+ element.cancel();
+ assert.isTrue(processorCancelStub.called);
+ });
+ });
+
+ suite('context hiding and expanding', () => {
+ let dispatchStub: sinon.SinonStub;
+
+ setup(async () => {
+ dispatchStub = sinon.stub(diffTable, 'dispatchEvent');
+ element.diff = {
+ ...createEmptyDiff(),
+ 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: KeyLocations = {left: {}, right: {}};
+ element.prefs = {
+ ...DEFAULT_PREFS,
+ context: 1,
+ };
+ element.render(keyLocations);
+ // Make sure all listeners are installed.
+ await element.untilGroupsRendered();
+ });
+
+ test('hides lines behind two context controls', () => {
+ const contextControls = diffTable.querySelectorAll('gr-context-controls');
+ assert.equal(contextControls.length, 2);
+
+ const diffRows = diffTable.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 = diffTable.querySelectorAll('gr-context-controls');
+ const topExpandCommonButton =
+ contextControls[0].shadowRoot?.querySelectorAll<HTMLElement>(
+ '.showContext'
+ )[0];
+ assert.isOk(topExpandCommonButton);
+ assert.include(topExpandCommonButton!.textContent, '+9 common lines');
+ topExpandCommonButton!.click();
+ const diffRows = diffTable.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', async () => {
+ dispatchStub.reset();
+ element.unhideLine(4, Side.LEFT);
+
+ const diffRows = diffTable.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');
+
+ await element.untilGroupsRendered();
+ const firedEventTypes = dispatchStub.getCalls().map(c => c.args[0].type);
+ assert.include(firedEventTypes, 'render-content');
+ });
+ });
+
+ suite('mock-diff', () => {
+ let builder: GrDiffBuilderSideBySide;
+ let diff: DiffInfo;
+ let keyLocations: KeyLocations;
+
+ setup(() => {
+ element.viewMode = DiffViewMode.SIDE_BY_SIDE;
+ diff = createDiff();
+ element.diff = diff;
+
+ keyLocations = {left: {}, right: {}};
+
+ element.prefs = {
+ ...createDefaultDiffPrefs(),
+ line_length: 80,
+ show_tabs: true,
+ tab_size: 4,
+ };
+ element.render(keyLocations);
+ builder = element.builder as GrDiffBuilderSideBySide;
+ });
+
+ test('aria-labels on added line numbers', () => {
+ const deltaLineNumberButton = diffTable.querySelectorAll(
+ '.lineNumButton.right'
+ )[5];
+
+ assert.isOk(deltaLineNumberButton);
+ assert.equal(deltaLineNumberButton.getAttribute('aria-label'), '5 added');
+ });
+
+ test('aria-labels on removed line numbers', () => {
+ const deltaLineNumberButton = diffTable.querySelectorAll(
+ '.lineNumButton.left'
+ )[10];
+
+ assert.isOk(deltaLineNumberButton);
+ assert.equal(
+ deltaLineNumberButton.getAttribute('aria-label'),
+ '10 removed'
+ );
+ });
+
+ test('getContentByLine', () => {
+ let actual: HTMLElement | null;
+
+ actual = builder.getContentByLine(2, Side.LEFT);
+ assert.equal(actual?.textContent, diff.content[0].ab?.[1]);
+
+ actual = builder.getContentByLine(2, Side.RIGHT);
+ assert.equal(actual?.textContent, diff.content[0].ab?.[1]);
+
+ actual = builder.getContentByLine(5, Side.LEFT);
+ assert.equal(actual?.textContent, diff.content[2].ab?.[0]);
+
+ actual = builder.getContentByLine(5, Side.RIGHT);
+ assert.equal(actual?.textContent, diff.content[1].b?.[0]);
+ });
+
+ test('getContentTdByLineEl works both with button and td', () => {
+ const diffRow = diffTable.querySelectorAll('tr.diff-row')[2];
+
+ const lineNumTdLeft = queryAndAssert(diffRow, 'td.lineNum.left');
+ const lineNumButtonLeft = queryAndAssert(lineNumTdLeft, 'button');
+ const contentTdLeft = diffRow.querySelectorAll('.content')[0];
+
+ const lineNumTdRight = queryAndAssert(diffRow, 'td.lineNum.right');
+ const lineNumButtonRight = queryAndAssert(lineNumTdRight, 'button');
+ const contentTdRight = diffRow.querySelectorAll('.content')[1];
+
+ assert.equal(element.getContentTdByLineEl(lineNumTdLeft), contentTdLeft);
+ assert.equal(
+ element.getContentTdByLineEl(lineNumButtonLeft),
+ contentTdLeft
+ );
+ assert.equal(
+ element.getContentTdByLineEl(lineNumTdRight),
+ contentTdRight
+ );
+ assert.equal(
+ element.getContentTdByLineEl(lineNumButtonRight),
+ contentTdRight
+ );
+ });
+
+ test('findLinesByRange', () => {
+ const lines: GrDiffLine[] = [];
+ const elems: HTMLElement[] = [];
+ const start = 6;
+ const end = 10;
+ const count = end - start + 1;
+
+ builder.findLinesByRange(start, end, Side.RIGHT, lines, elems);
+
+ assert.equal(lines.length, count);
+ assert.equal(elems.length, count);
+
+ for (let i = 0; i < 5; i++) {
+ assert.instanceOf(lines[i], GrDiffLine);
+ assert.equal(lines[i].afterNumber, start + i);
+ assert.instanceOf(elems[i], HTMLElement);
+ assert.equal(lines[i].text, elems[i].textContent);
+ }
+ });
+
+ test('renderContentByRange', () => {
+ const spy = sinon.spy(builder, 'createTextEl');
+ const start = 9;
+ const end = 14;
+ const count = end - start + 1;
+
+ builder.renderContentByRange(start, end, Side.LEFT);
+
+ assert.equal(spy.callCount, count);
+ spy.getCalls().forEach((call, i: number) => {
+ assert.equal(call.args[1].beforeNumber, start + i);
+ });
+ });
+
+ test('renderContentByRange non-existent elements', () => {
+ const spy = sinon.spy(builder, 'createTextEl');
+
+ sinon
+ .stub(builder, 'getLineNumberEl')
+ .returns(document.createElement('div'));
+ sinon
+ .stub(builder, 'findLinesByRange')
+ .callsFake((_1, _2, _3, lines, elements) => {
+ // Add a line and a corresponding element.
+ lines?.push(new GrDiffLine(GrDiffLineType.BOTH));
+ const tr = document.createElement('tr');
+ const td = document.createElement('td');
+ const el = document.createElement('div');
+ tr.appendChild(td);
+ td.appendChild(el);
+ elements?.push(el);
+
+ // Add 2 lines without corresponding elements.
+ lines?.push(new GrDiffLine(GrDiffLineType.BOTH));
+ lines?.push(new GrDiffLine(GrDiffLineType.BOTH));
+ });
+
+ builder.renderContentByRange(1, 10, Side.LEFT);
+ // Should be called only once because only one line had a corresponding
+ // element.
+ assert.equal(spy.callCount, 1);
+ });
+
+ test('getLineNumberEl side-by-side left', () => {
+ const contentEl = builder.getContentByLine(
+ 5,
+ Side.LEFT,
+ element.diffElement as HTMLTableElement
+ );
+ assert.isOk(contentEl);
+ const lineNumberEl = builder.getLineNumberEl(contentEl!, Side.LEFT);
+ assert.isOk(lineNumberEl);
+ assert.isTrue(lineNumberEl!.classList.contains('lineNum'));
+ assert.isTrue(lineNumberEl!.classList.contains(Side.LEFT));
+ });
+
+ test('getLineNumberEl side-by-side right', () => {
+ const contentEl = builder.getContentByLine(
+ 5,
+ Side.RIGHT,
+ element.diffElement as HTMLTableElement
+ );
+ assert.isOk(contentEl);
+ const lineNumberEl = builder.getLineNumberEl(contentEl!, Side.RIGHT);
+ assert.isOk(lineNumberEl);
+ assert.isTrue(lineNumberEl!.classList.contains('lineNum'));
+ assert.isTrue(lineNumberEl!.classList.contains(Side.RIGHT));
+ });
+
+ test('getLineNumberEl unified left', async () => {
+ // Re-render as unified:
+ element.viewMode = 'UNIFIED_DIFF';
+ element.render(keyLocations);
+ builder = element.builder as GrDiffBuilderSideBySide;
+
+ const contentEl = builder.getContentByLine(
+ 5,
+ Side.LEFT,
+ element.diffElement as HTMLTableElement
+ );
+ assert.isOk(contentEl);
+ const lineNumberEl = builder.getLineNumberEl(contentEl!, Side.LEFT);
+ assert.isOk(lineNumberEl);
+ assert.isTrue(lineNumberEl!.classList.contains('lineNum'));
+ assert.isTrue(lineNumberEl!.classList.contains(Side.LEFT));
+ });
+
+ test('getLineNumberEl unified right', async () => {
+ // Re-render as unified:
+ element.viewMode = 'UNIFIED_DIFF';
+ element.render(keyLocations);
+ builder = element.builder as GrDiffBuilderSideBySide;
+
+ const contentEl = builder.getContentByLine(
+ 5,
+ Side.RIGHT,
+ element.diffElement as HTMLTableElement
+ );
+ assert.isOk(contentEl);
+ const lineNumberEl = builder.getLineNumberEl(contentEl!, Side.RIGHT);
+ assert.isOk(lineNumberEl);
+ assert.isTrue(lineNumberEl!.classList.contains('lineNum'));
+ assert.isTrue(lineNumberEl!.classList.contains(Side.RIGHT));
+ });
+
+ test('getNextContentOnSide side-by-side left', () => {
+ const startElem = builder.getContentByLine(
+ 5,
+ Side.LEFT,
+ element.diffElement as HTMLTableElement
+ );
+ assert.isOk(startElem);
+ const expectedStartString = diff.content[2].ab?.[0];
+ const expectedNextString = diff.content[2].ab?.[1];
+ assert.equal(startElem!.textContent, expectedStartString);
+
+ const nextElem = builder.getNextContentOnSide(startElem!, Side.LEFT);
+ assert.isOk(nextElem);
+ assert.equal(nextElem!.textContent, expectedNextString);
+ });
+
+ test('getNextContentOnSide side-by-side right', () => {
+ const startElem = builder.getContentByLine(
+ 5,
+ Side.RIGHT,
+ element.diffElement as HTMLTableElement
+ );
+ const expectedStartString = diff.content[1].b?.[0];
+ const expectedNextString = diff.content[1].b?.[1];
+ assert.isOk(startElem);
+ assert.equal(startElem!.textContent, expectedStartString);
+
+ const nextElem = builder.getNextContentOnSide(startElem!, Side.RIGHT);
+ assert.isOk(nextElem);
+ assert.equal(nextElem!.textContent, expectedNextString);
+ });
+
+ test('getNextContentOnSide unified left', async () => {
+ // Re-render as unified:
+ element.viewMode = 'UNIFIED_DIFF';
+ element.render(keyLocations);
+ builder = element.builder as GrDiffBuilderSideBySide;
+
+ const startElem = builder.getContentByLine(
+ 5,
+ Side.LEFT,
+ element.diffElement as HTMLTableElement
+ );
+ const expectedStartString = diff.content[2].ab?.[0];
+ const expectedNextString = diff.content[2].ab?.[1];
+ assert.isOk(startElem);
+ assert.equal(startElem!.textContent, expectedStartString);
+
+ const nextElem = builder.getNextContentOnSide(startElem!, Side.LEFT);
+ assert.isOk(nextElem);
+ assert.equal(nextElem!.textContent, expectedNextString);
+ });
+
+ test('getNextContentOnSide unified right', async () => {
+ // Re-render as unified:
+ element.viewMode = 'UNIFIED_DIFF';
+ element.render(keyLocations);
+ builder = element.builder as GrDiffBuilderSideBySide;
+
+ const startElem = builder.getContentByLine(
+ 5,
+ Side.RIGHT,
+ element.diffElement as HTMLTableElement
+ );
+ const expectedStartString = diff.content[1].b?.[0];
+ const expectedNextString = diff.content[1].b?.[1];
+ assert.isOk(startElem);
+ assert.equal(startElem!.textContent, expectedStartString);
+
+ const nextElem = builder.getNextContentOnSide(startElem!, Side.RIGHT);
+ assert.isOk(nextElem);
+ assert.equal(nextElem!.textContent, expectedNextString);
+ });
+ });
+
+ suite('blame', () => {
+ let mockBlame: BlameInfo[];
+
+ setup(() => {
+ mockBlame = [
+ {
+ author: 'test-author',
+ time: 314,
+ commit_msg: 'test-commit-message',
+ id: 'commit 1',
+ ranges: [
+ {start: 1, end: 2},
+ {start: 10, end: 16},
+ ],
+ },
+ {
+ author: 'test-author',
+ time: 314,
+ commit_msg: 'test-commit-message',
+ id: 'commit 2',
+ ranges: [
+ {start: 4, end: 10},
+ {start: 17, end: 32},
+ ],
+ },
+ ];
+ });
+
+ test('setBlame attempts to render each blamed line', () => {
+ const getBlameStub = sinon
+ .stub(builder, 'getBlameTdByLine')
+ .returns(undefined);
+ builder.setBlame(mockBlame);
+ assert.equal(getBlameStub.callCount, 32);
+ });
+
+ test('getBlameCommitForBaseLine', () => {
+ sinon.stub(builder, 'getBlameTdByLine').returns(undefined);
+ builder.setBlame(mockBlame);
+ assert.isOk(builder.getBlameCommitForBaseLine(1));
+ assert.equal(builder.getBlameCommitForBaseLine(1)?.id, 'commit 1');
+
+ assert.isOk(builder.getBlameCommitForBaseLine(11));
+ assert.equal(builder.getBlameCommitForBaseLine(11)?.id, 'commit 1');
+
+ assert.isOk(builder.getBlameCommitForBaseLine(32));
+ assert.equal(builder.getBlameCommitForBaseLine(32)?.id, 'commit 2');
+
+ assert.isUndefined(builder.getBlameCommitForBaseLine(33));
+ });
+
+ test('getBlameCommitForBaseLine w/o blame returns null', () => {
+ assert.isUndefined(builder.getBlameCommitForBaseLine(1));
+ assert.isUndefined(builder.getBlameCommitForBaseLine(11));
+ assert.isUndefined(builder.getBlameCommitForBaseLine(31));
+ });
+
+ test('createBlameCell', () => {
+ const mockBlameInfo = {
+ time: 1576155200,
+ id: '1234567890',
+ author: 'Clark Kent',
+ commit_msg: 'Testing Commit',
+ ranges: [{start: 4, end: 10}],
+ };
+ const getBlameStub = sinon
+ .stub(builder, 'getBlameCommitForBaseLine')
+ .returns(mockBlameInfo);
+ const line = new GrDiffLine(GrDiffLineType.BOTH);
+ line.beforeNumber = 3;
+ line.afterNumber = 5;
+
+ const result = builder.createBlameCell(line.beforeNumber);
+
+ assert.isTrue(getBlameStub.calledWithExactly(3));
+ assert.equal(result.getAttribute('data-line-number'), '3');
+ expect(result).dom.to.equal(/* HTML */ `
+ <span class="gr-diff style-scope">
+ <a class="blameDate gr-diff style-scope" href="/r/q/1234567890">
+ 12/12/2019
+ </a>
+ <span class="blameAuthor gr-diff style-scope">Clark</span>
+ <gr-hovercard class="gr-diff style-scope">
+ <span class="blameHoverCard gr-diff style-scope">
+ Commit 1234567890<br />
+ Author: Clark Kent<br />
+ Date: 12/12/2019<br />
+ <br />
+ Testing Commit
+ </span>
+ </gr-hovercard>
+ </span>
+ `);
+ });
+ });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
index ceadc94..c04d156 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-legacy.ts
@@ -162,10 +162,8 @@
*
* TODO(brohlfs): Consolidate this with getLineEl... methods in html file.
*/
- private getLineNumberEl(
- content: HTMLElement,
- side: Side
- ): HTMLElement | null {
+ // visible for testing
+ getLineNumberEl(content: HTMLElement, side: Side): HTMLElement | null {
let row: HTMLElement | null = content;
while (row && !row.classList.contains('diff-row')) row = row.parentElement;
return row ? (row.querySelector('.lineNum.' + side) as HTMLElement) : null;
@@ -349,7 +347,8 @@
});
}
- protected createTextEl(
+ // visible for testing
+ createTextEl(
lineNumberEl: HTMLElement | null,
line: GrDiffLine,
side?: Side
@@ -491,7 +490,8 @@
* Create a blame cell for the given base line. Blame information will be
* included in the cell if available.
*/
- protected createBlameCell(lineNumber: LineNumber): HTMLTableCellElement {
+ // visible for testing
+ createBlameCell(lineNumber: LineNumber): HTMLTableCellElement {
const blameTd = createElementDiff('td', 'blame') as HTMLTableCellElement;
blameTd.setAttribute('data-line-number', lineNumber.toString());
if (!lineNumber) return blameTd;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
index a711215..f2690bc 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-side-by-side.ts
@@ -44,7 +44,8 @@
};
}
- protected override buildSectionElement(group: GrDiffGroup) {
+ // visible for testing
+ override buildSectionElement(group: GrDiffGroup) {
const sectionEl = createElementDiff('tbody', 'section');
sectionEl.classList.add(group.type);
if (group.isTotal()) {
@@ -147,7 +148,8 @@
return td;
}
- protected override getNextContentOnSide(
+ // visible for testing
+ override getNextContentOnSide(
content: HTMLElement,
side: Side
): HTMLElement | null {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified.ts
index 4145485..0c9d1d9 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified.ts
@@ -43,7 +43,8 @@
};
}
- protected override buildSectionElement(group: GrDiffGroup): HTMLElement {
+ // visible for testing
+ override buildSectionElement(group: GrDiffGroup): HTMLElement {
const sectionEl = createElementDiff('tbody', 'section');
sectionEl.classList.add(group.type);
if (group.isTotal()) {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.js b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.js
deleted file mode 100644
index 5f3fb72..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.js
+++ /dev/null
@@ -1,242 +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-diff/gr-diff-group.js';
-import './gr-diff-builder.js';
-import './gr-diff-builder-unified.js';
-import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line.js';
-import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group.js';
-import {GrDiffBuilderUnified} from './gr-diff-builder-unified.js';
-
-suite('GrDiffBuilderUnified tests', () => {
- let prefs;
- let outputEl;
- let diffBuilder;
-
- setup(()=> {
- prefs = {
- line_length: 10,
- show_tabs: true,
- tab_size: 4,
- };
- outputEl = document.createElement('div');
- diffBuilder = new GrDiffBuilderUnified({}, prefs, outputEl, []);
- });
-
- suite('buildSectionElement for BOTH group', () => {
- let lines;
- let group;
-
- setup(() => {
- lines = [
- new GrDiffLine(GrDiffLineType.BOTH, 1, 2),
- new GrDiffLine(GrDiffLineType.BOTH, 2, 3),
- new GrDiffLine(GrDiffLineType.BOTH, 3, 4),
- ];
- lines[0].text = 'def hello_world():';
- lines[1].text = ' print "Hello World";';
- lines[2].text = ' return True';
-
- group = new GrDiffGroup({type: GrDiffGroupType.BOTH, lines});
- });
-
- test('creates the section', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('section'));
- assert.isTrue(sectionEl.classList.contains('both'));
- });
-
- test('creates each unchanged row once', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- const rowEls = sectionEl.querySelectorAll('.diff-row');
-
- assert.equal(rowEls.length, 3);
-
- assert.equal(
- rowEls[0].querySelector('.lineNum.left').textContent,
- lines[0].beforeNumber);
- assert.equal(
- rowEls[0].querySelector('.lineNum.right').textContent,
- lines[0].afterNumber);
- assert.equal(
- rowEls[0].querySelector('.content').textContent, lines[0].text);
-
- assert.equal(
- rowEls[1].querySelector('.lineNum.left').textContent,
- lines[1].beforeNumber);
- assert.equal(
- rowEls[1].querySelector('.lineNum.right').textContent,
- lines[1].afterNumber);
- assert.equal(
- rowEls[1].querySelector('.content').textContent, lines[1].text);
-
- assert.equal(
- rowEls[2].querySelector('.lineNum.left').textContent,
- lines[2].beforeNumber);
- assert.equal(
- rowEls[2].querySelector('.lineNum.right').textContent,
- lines[2].afterNumber);
- assert.equal(
- rowEls[2].querySelector('.content').textContent, lines[2].text);
- });
- });
-
- suite('buildSectionElement for moved chunks', () => {
- test('creates a moved out group', () => {
- const lines = [
- new GrDiffLine(GrDiffLineType.REMOVE, 15),
- new GrDiffLine(GrDiffLineType.REMOVE, 16),
- ];
- lines[0].text = 'def hello_world():';
- lines[1].text = ' print "Hello World"';
- const group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
- group.moveDetails = {changed: false};
-
- const sectionEl = diffBuilder.buildSectionElement(group);
-
- const rowEls = sectionEl.querySelectorAll('tr');
- const moveControlsRow = rowEls[0];
- const cells = moveControlsRow.querySelectorAll('td');
- assert.isTrue(sectionEl.classList.contains('dueToMove'));
- assert.equal(rowEls.length, 3);
- assert.isTrue(moveControlsRow.classList.contains('movedOut'));
- assert.equal(cells.length, 3);
- assert.isTrue(cells[2].classList.contains('moveHeader'));
- assert.equal(cells[2].textContent, 'Moved out');
- });
-
- test('creates a moved in group', () => {
- const lines = [
- new GrDiffLine(GrDiffLineType.ADD, 37),
- new GrDiffLine(GrDiffLineType.ADD, 38),
- ];
- lines[0].text = 'def hello_world():';
- lines[1].text = ' print "Hello World"';
- const group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
- group.moveDetails = {changed: false};
-
- const sectionEl = diffBuilder.buildSectionElement(group);
-
- const rowEls = sectionEl.querySelectorAll('tr');
- const moveControlsRow = rowEls[0];
- const cells = moveControlsRow.querySelectorAll('td');
- assert.isTrue(sectionEl.classList.contains('dueToMove'));
- assert.equal(rowEls.length, 3);
- assert.isTrue(moveControlsRow.classList.contains('movedIn'));
- assert.equal(cells.length, 3);
- assert.isTrue(cells[2].classList.contains('moveHeader'));
- assert.equal(cells[2].textContent, 'Moved in');
- });
- });
-
- suite('buildSectionElement for DELTA group', () => {
- let lines;
- let group;
-
- setup(() => {
- lines = [
- new GrDiffLine(GrDiffLineType.REMOVE, 1),
- new GrDiffLine(GrDiffLineType.REMOVE, 2),
- new GrDiffLine(GrDiffLineType.ADD, 2),
- new GrDiffLine(GrDiffLineType.ADD, 3),
- ];
- lines[0].text = 'def hello_world():';
- lines[1].text = ' print "Hello World"';
- lines[2].text = 'def hello_universe()';
- lines[3].text = ' print "Hello Universe"';
-
- group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
- });
-
- test('creates the section', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('section'));
- assert.isTrue(sectionEl.classList.contains('delta'));
- });
-
- test('creates the section with class if ignoredWhitespaceOnly', () => {
- group.ignoredWhitespaceOnly = true;
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('ignoredWhitespaceOnly'));
- });
-
- test('creates the section with class if dueToRebase', () => {
- group.dueToRebase = true;
- const sectionEl = diffBuilder.buildSectionElement(group);
- assert.isTrue(sectionEl.classList.contains('dueToRebase'));
- });
-
- test('creates first the removed and then the added rows', () => {
- const sectionEl = diffBuilder.buildSectionElement(group);
- const rowEls = sectionEl.querySelectorAll('.diff-row');
-
- assert.equal(rowEls.length, 4);
-
- assert.equal(
- rowEls[0].querySelector('.lineNum.left').textContent,
- lines[0].beforeNumber);
- assert.isNotOk(rowEls[0].querySelector('.lineNum.right'));
- assert.equal(
- rowEls[0].querySelector('.content').textContent, lines[0].text);
-
- assert.equal(
- rowEls[1].querySelector('.lineNum.left').textContent,
- lines[1].beforeNumber);
- assert.isNotOk(rowEls[1].querySelector('.lineNum.right'));
- assert.equal(
- rowEls[1].querySelector('.content').textContent, lines[1].text);
-
- assert.isNotOk(rowEls[2].querySelector('.lineNum.left'));
- assert.equal(
- rowEls[2].querySelector('.lineNum.right').textContent,
- lines[2].afterNumber);
- assert.equal(
- rowEls[2].querySelector('.content').textContent, lines[2].text);
-
- assert.isNotOk(rowEls[3].querySelector('.lineNum.left'));
- assert.equal(
- rowEls[3].querySelector('.lineNum.right').textContent,
- lines[3].afterNumber);
- assert.equal(
- rowEls[3].querySelector('.content').textContent, lines[3].text);
- });
-
- test('creates only the added rows if only ignored whitespace', () => {
- group.ignoredWhitespaceOnly = true;
- const sectionEl = diffBuilder.buildSectionElement(group);
- const rowEls = sectionEl.querySelectorAll('.diff-row');
-
- assert.equal(rowEls.length, 2);
-
- assert.isNotOk(rowEls[0].querySelector('.lineNum.left'));
- assert.equal(
- rowEls[0].querySelector('.lineNum.right').textContent,
- lines[2].afterNumber);
- assert.equal(
- rowEls[0].querySelector('.content').textContent, lines[2].text);
-
- assert.isNotOk(rowEls[1].querySelector('.lineNum.left'));
- assert.equal(
- rowEls[1].querySelector('.lineNum.right').textContent,
- lines[3].afterNumber);
- assert.equal(
- rowEls[1].querySelector('.content').textContent, lines[3].text);
- });
- });
-});
-
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.ts
new file mode 100644
index 0000000..7a9d06d
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder-unified_test.ts
@@ -0,0 +1,282 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import '../gr-diff/gr-diff-group';
+import './gr-diff-builder';
+import './gr-diff-builder-unified';
+import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line';
+import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
+import {GrDiffBuilderUnified} from './gr-diff-builder-unified';
+import {DiffPreferencesInfo} from '../../../api/diff';
+import {createDefaultDiffPrefs} from '../../../constants/constants';
+import {createDiff} from '../../../test/test-data-generators';
+import {queryAndAssert} from '../../../utils/common-util';
+
+suite('GrDiffBuilderUnified tests', () => {
+ let prefs: DiffPreferencesInfo;
+ let outputEl: HTMLElement;
+ let diffBuilder: GrDiffBuilderUnified;
+
+ setup(() => {
+ prefs = {
+ ...createDefaultDiffPrefs(),
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ };
+ outputEl = document.createElement('div');
+ diffBuilder = new GrDiffBuilderUnified(createDiff(), prefs, outputEl, []);
+ });
+
+ suite('buildSectionElement for BOTH group', () => {
+ let lines: GrDiffLine[];
+ let group: GrDiffGroup;
+
+ setup(() => {
+ lines = [
+ new GrDiffLine(GrDiffLineType.BOTH, 1, 2),
+ new GrDiffLine(GrDiffLineType.BOTH, 2, 3),
+ new GrDiffLine(GrDiffLineType.BOTH, 3, 4),
+ ];
+ lines[0].text = 'def hello_world():';
+ lines[1].text = ' print "Hello World";';
+ lines[2].text = ' return True';
+
+ group = new GrDiffGroup({type: GrDiffGroupType.BOTH, lines});
+ });
+
+ test('creates the section', () => {
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ assert.isTrue(sectionEl.classList.contains('section'));
+ assert.isTrue(sectionEl.classList.contains('both'));
+ });
+
+ test('creates each unchanged row once', () => {
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ const rowEls = sectionEl.querySelectorAll('.diff-row');
+
+ assert.equal(rowEls.length, 3);
+
+ assert.equal(
+ queryAndAssert(rowEls[0], '.lineNum.left').textContent,
+ lines[0].beforeNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[0], '.lineNum.right').textContent,
+ lines[0].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[0], '.content').textContent,
+ lines[0].text
+ );
+
+ assert.equal(
+ queryAndAssert(rowEls[1], '.lineNum.left').textContent,
+ lines[1].beforeNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[1], '.lineNum.right').textContent,
+ lines[1].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[1], '.content').textContent,
+ lines[1].text
+ );
+
+ assert.equal(
+ queryAndAssert(rowEls[2], '.lineNum.left').textContent,
+ lines[2].beforeNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[2], '.lineNum.right').textContent,
+ lines[2].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[2], '.content').textContent,
+ lines[2].text
+ );
+ });
+ });
+
+ suite('buildSectionElement for moved chunks', () => {
+ test('creates a moved out group', () => {
+ const lines = [
+ new GrDiffLine(GrDiffLineType.REMOVE, 15),
+ new GrDiffLine(GrDiffLineType.REMOVE, 16),
+ ];
+ lines[0].text = 'def hello_world():';
+ lines[1].text = ' print "Hello World"';
+ const group = new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines,
+ moveDetails: {changed: false},
+ });
+
+ const sectionEl = diffBuilder.buildSectionElement(group);
+
+ const rowEls = sectionEl.querySelectorAll('tr');
+ const moveControlsRow = rowEls[0];
+ const cells = moveControlsRow.querySelectorAll('td');
+ assert.isTrue(sectionEl.classList.contains('dueToMove'));
+ assert.equal(rowEls.length, 3);
+ assert.isTrue(moveControlsRow.classList.contains('movedOut'));
+ assert.equal(cells.length, 3);
+ assert.isTrue(cells[2].classList.contains('moveHeader'));
+ assert.equal(cells[2].textContent, 'Moved out');
+ });
+
+ test('creates a moved in group', () => {
+ const lines = [
+ new GrDiffLine(GrDiffLineType.ADD, 37),
+ new GrDiffLine(GrDiffLineType.ADD, 38),
+ ];
+ lines[0].text = 'def hello_world():';
+ lines[1].text = ' print "Hello World"';
+ const group = new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines,
+ moveDetails: {changed: false},
+ });
+
+ const sectionEl = diffBuilder.buildSectionElement(group);
+
+ const rowEls = sectionEl.querySelectorAll('tr');
+ const moveControlsRow = rowEls[0];
+ const cells = moveControlsRow.querySelectorAll('td');
+ assert.isTrue(sectionEl.classList.contains('dueToMove'));
+ assert.equal(rowEls.length, 3);
+ assert.isTrue(moveControlsRow.classList.contains('movedIn'));
+ assert.equal(cells.length, 3);
+ assert.isTrue(cells[2].classList.contains('moveHeader'));
+ assert.equal(cells[2].textContent, 'Moved in');
+ });
+ });
+
+ suite('buildSectionElement for DELTA group', () => {
+ let lines: GrDiffLine[];
+ let group: GrDiffGroup;
+
+ setup(() => {
+ lines = [
+ new GrDiffLine(GrDiffLineType.REMOVE, 1),
+ new GrDiffLine(GrDiffLineType.REMOVE, 2),
+ new GrDiffLine(GrDiffLineType.ADD, 2),
+ new GrDiffLine(GrDiffLineType.ADD, 3),
+ ];
+ lines[0].text = 'def hello_world():';
+ lines[1].text = ' print "Hello World"';
+ lines[2].text = 'def hello_universe()';
+ lines[3].text = ' print "Hello Universe"';
+ });
+
+ test('creates the section', () => {
+ group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ assert.isTrue(sectionEl.classList.contains('section'));
+ assert.isTrue(sectionEl.classList.contains('delta'));
+ });
+
+ test('creates the section with class if ignoredWhitespaceOnly', () => {
+ group = new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines,
+ ignoredWhitespaceOnly: true,
+ });
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ assert.isTrue(sectionEl.classList.contains('ignoredWhitespaceOnly'));
+ });
+
+ test('creates the section with class if dueToRebase', () => {
+ group = new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines,
+ dueToRebase: true,
+ });
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ assert.isTrue(sectionEl.classList.contains('dueToRebase'));
+ });
+
+ test('creates first the removed and then the added rows', () => {
+ group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ const rowEls = sectionEl.querySelectorAll('.diff-row');
+
+ assert.equal(rowEls.length, 4);
+
+ assert.equal(
+ queryAndAssert(rowEls[0], '.lineNum.left').textContent,
+ lines[0].beforeNumber.toString()
+ );
+ assert.isNotOk(rowEls[0].querySelector('.lineNum.right'));
+ assert.equal(
+ queryAndAssert(rowEls[0], '.content').textContent,
+ lines[0].text
+ );
+
+ assert.equal(
+ queryAndAssert(rowEls[1], '.lineNum.left').textContent,
+ lines[1].beforeNumber.toString()
+ );
+ assert.isNotOk(rowEls[1].querySelector('.lineNum.right'));
+ assert.equal(
+ queryAndAssert(rowEls[1], '.content').textContent,
+ lines[1].text
+ );
+
+ assert.isNotOk(rowEls[2].querySelector('.lineNum.left'));
+ assert.equal(
+ queryAndAssert(rowEls[2], '.lineNum.right').textContent,
+ lines[2].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[2], '.content').textContent,
+ lines[2].text
+ );
+
+ assert.isNotOk(rowEls[3].querySelector('.lineNum.left'));
+ assert.equal(
+ queryAndAssert(rowEls[3], '.lineNum.right').textContent,
+ lines[3].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[3], '.content').textContent,
+ lines[3].text
+ );
+ });
+
+ test('creates only the added rows if only ignored whitespace', () => {
+ group = new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines,
+ ignoredWhitespaceOnly: true,
+ });
+ const sectionEl = diffBuilder.buildSectionElement(group);
+ const rowEls = sectionEl.querySelectorAll('.diff-row');
+
+ assert.equal(rowEls.length, 2);
+
+ assert.isNotOk(rowEls[0].querySelector('.lineNum.left'));
+ assert.equal(
+ queryAndAssert(rowEls[0], '.lineNum.right').textContent,
+ lines[2].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[0], '.content').textContent,
+ lines[2].text
+ );
+
+ assert.isNotOk(rowEls[1].querySelector('.lineNum.left'));
+ assert.equal(
+ queryAndAssert(rowEls[1], '.lineNum.right').textContent,
+ lines[3].afterNumber.toString()
+ );
+ assert.equal(
+ queryAndAssert(rowEls[1], '.content').textContent,
+ lines[3].text
+ );
+ });
+ });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
index add7ffa..4b664e2 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-builder.ts
@@ -89,7 +89,8 @@
protected readonly numLinesLeft: number;
- protected readonly _prefs: DiffPreferencesInfo;
+ // visible for testing
+ readonly _prefs: DiffPreferencesInfo;
protected readonly renderPrefs?: RenderPreferences;
@@ -194,7 +195,8 @@
group.element = element;
}
- private getGroupsByLineRange(
+ // visible for testing
+ getGroupsByLineRange(
startLine: LineNumber,
endLine: LineNumber,
side: Side
@@ -257,7 +259,8 @@
* TODO: Change `null` to `undefined` in paramete type. Also: Do we
* really need to support null/undefined? Also change to camelCase.
*/
- protected findLinesByRange(
+ // visible for testing
+ findLinesByRange(
start: LineNumber,
end: LineNumber,
side: Side,
@@ -352,9 +355,8 @@
*
* @return The commit information.
*/
- protected getBlameCommitForBaseLine(
- lineNum: LineNumber
- ): BlameInfo | undefined {
+ // visible for testing
+ getBlameCommitForBaseLine(lineNum: LineNumber): BlameInfo | undefined {
for (const blameCommit of this.blameInfo) {
for (const range of blameCommit.ranges) {
if (range.start <= lineNum && range.end >= lineNum) {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts
index dfe8a15..e00353a 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor.ts
@@ -109,7 +109,8 @@
*/
initialLineNumber: number | null = null;
- private cursorManager = new GrCursorManager();
+ // visible for testing
+ cursorManager = new GrCursorManager();
private targetSubscription?: Subscription;
@@ -335,10 +336,6 @@
this.preventAutoScrollOnManualScroll = true;
};
- private _boundHandleDiffRenderProgress = () => {
- this._updateStops();
- };
-
private _boundHandleDiffRenderContent = () => {
this._updateStops();
// When done rendering, turn focus on move and automatic scrolling back on
@@ -550,10 +547,6 @@
);
diff.removeEventListener('render-start', this._boundHandleDiffRenderStart);
diff.removeEventListener(
- 'render-progress',
- this._boundHandleDiffRenderProgress
- );
- diff.removeEventListener(
'render-content',
this._boundHandleDiffRenderContent
);
@@ -569,10 +562,6 @@
this.boundHandleDiffLoadingChanged
);
diff.addEventListener('render-start', this._boundHandleDiffRenderStart);
- diff.addEventListener(
- 'render-progress',
- this._boundHandleDiffRenderProgress
- );
diff.addEventListener('render-content', this._boundHandleDiffRenderContent);
diff.addEventListener('line-selected', this._boundHandleDiffLineSelected);
}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.js b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.js
deleted file mode 100644
index f48d673..0000000
--- a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.js
+++ /dev/null
@@ -1,681 +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-diff/gr-diff.js';
-import './gr-diff-cursor.js';
-import {fixture, html} from '@open-wc/testing-helpers';
-import {listenOnce, mockPromise} from '../../../test/test-utils.js';
-import {createDiff} from '../../../test/test-data-generators.js';
-import {createDefaultDiffPrefs} from '../../../constants/constants.js';
-import {GrDiffCursor} from './gr-diff-cursor.js';
-import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
-
-suite('gr-diff-cursor tests', () => {
- let cursor;
- let diffElement;
- let diff;
-
- setup(async () => {
- diffElement = await fixture(html`<gr-diff></gr-diff>`);
- cursor = new GrDiffCursor();
-
- // Register the diff with the cursor.
- cursor.replaceDiffs([diffElement]);
-
- diffElement.loggedIn = false;
- diffElement.comments = {
- left: [],
- right: [],
- meta: {},
- };
- diffElement.path = 'some/path.ts';
- const promise = mockPromise();
- const setupDone = () => {
- cursor._updateStops();
- cursor.moveToFirstChunk();
- diffElement.removeEventListener('render', setupDone);
- promise.resolve();
- };
- diffElement.addEventListener('render', setupDone);
-
- diff = createDiff();
- diffElement.prefs = createDefaultDiffPrefs();
- diffElement.diff = diff;
- await promise;
- });
-
- test('diff cursor functionality (side-by-side)', () => {
- // The cursor has been initialized to the first delta.
- assert.isOk(cursor.diffRow);
-
- const firstDeltaRow = diffElement.shadowRoot
- .querySelector('.section.delta .diff-row');
- assert.equal(cursor.diffRow, firstDeltaRow);
-
- cursor.moveDown();
-
- assert.notEqual(cursor.diffRow, firstDeltaRow);
- assert.equal(cursor.diffRow, firstDeltaRow.nextSibling);
-
- cursor.moveUp();
-
- assert.notEqual(cursor.diffRow, firstDeltaRow.nextSibling);
- assert.equal(cursor.diffRow, firstDeltaRow);
- });
-
- test('moveToFirstChunk', async () => {
- const diff = {
- meta_a: {
- name: 'lorem-ipsum.txt',
- content_type: 'text/plain',
- lines: 3,
- },
- meta_b: {
- name: 'lorem-ipsum.txt',
- content_type: 'text/plain',
- lines: 3,
- },
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- diff_header: [
- 'diff --git a/lorem-ipsum.txt b/lorem-ipsum.txt',
- 'index b2adcf4..554ae49 100644',
- '--- a/lorem-ipsum.txt',
- '+++ b/lorem-ipsum.txt',
- ],
- content: [
- {b: ['new line 1']},
- {ab: ['unchanged line']},
- {a: ['old line 2']},
- {ab: ['more unchanged lines']},
- ],
- };
-
- diffElement.diff = diff;
- // The file comment button, if present, is a cursor stop. Ensure
- // moveToFirstChunk() works correctly even if the button is not shown.
- diffElement.prefs.show_file_comment_button = false;
- await flush();
- cursor._updateStops();
-
- const chunks = Array.from(diffElement.root.querySelectorAll(
- '.section.delta'));
- assert.equal(chunks.length, 2);
-
- // Verify it works on fresh diff.
- cursor.moveToFirstChunk();
- assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 0);
- assert.equal(cursor.side, 'right');
-
- // Verify it works from other cursor positions.
- cursor.moveToNextChunk();
- assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 1);
- assert.equal(cursor.side, 'left');
- cursor.moveToFirstChunk();
- assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 0);
- assert.equal(cursor.side, 'right');
- });
-
- test('moveToLastChunk', async () => {
- const diff = {
- meta_a: {
- name: 'lorem-ipsum.txt',
- content_type: 'text/plain',
- lines: 3,
- },
- meta_b: {
- name: 'lorem-ipsum.txt',
- content_type: 'text/plain',
- lines: 3,
- },
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- diff_header: [
- 'diff --git a/lorem-ipsum.txt b/lorem-ipsum.txt',
- 'index b2adcf4..554ae49 100644',
- '--- a/lorem-ipsum.txt',
- '+++ b/lorem-ipsum.txt',
- ],
- content: [
- {ab: ['unchanged line']},
- {a: ['old line 2']},
- {ab: ['more unchanged lines']},
- {b: ['new line 3']},
- ],
- };
-
- diffElement.diff = diff;
- await new Promise(resolve => afterNextRender(diffElement, resolve));
- cursor._updateStops();
-
- const chunks = Array.from(diffElement.root.querySelectorAll(
- '.section.delta'));
- assert.equal(chunks.length, 2);
-
- // Verify it works on fresh diff.
- cursor.moveToLastChunk();
- assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 1);
- assert.equal(cursor.side, 'right');
-
- // Verify it works from other cursor positions.
- cursor.moveToPreviousChunk();
- assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 0);
- assert.equal(cursor.side, 'left');
- cursor.moveToLastChunk();
- assert.equal(chunks.indexOf(cursor.diffRow.parentElement), 1);
- assert.equal(cursor.side, 'right');
- });
-
- test('cursor scroll behavior', () => {
- assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
-
- diffElement.dispatchEvent(new Event('render-start'));
- assert.isTrue(cursor.cursorManager.focusOnMove);
-
- window.dispatchEvent(new Event('scroll'));
- assert.equal(cursor.cursorManager.scrollMode, 'never');
- assert.isFalse(cursor.cursorManager.focusOnMove);
-
- diffElement.dispatchEvent(new Event('render-content'));
- assert.isTrue(cursor.cursorManager.focusOnMove);
-
- cursor.reInitCursor();
- assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
- });
-
- test('moves to selected line', () => {
- const moveToNumStub = sinon.stub(cursor, 'moveToLineNumber');
-
- diffElement.dispatchEvent(
- new CustomEvent('line-selected', {
- detail: {number: '123', side: 'right', path: 'some/file'},
- }));
-
- assert.isTrue(moveToNumStub.called);
- assert.equal(moveToNumStub.lastCall.args[0], '123');
- assert.equal(moveToNumStub.lastCall.args[1], 'right');
- assert.equal(moveToNumStub.lastCall.args[2], 'some/file');
- });
-
- suite('unified diff', () => {
- setup(async () => {
- diffElement.viewMode = 'UNIFIED_DIFF';
- // We must allow the diff to re-render after setting the viewMode.
- await new Promise(resolve => afterNextRender(diffElement, resolve));
- cursor.reInitCursor();
- });
-
- test('diff cursor functionality (unified)', () => {
- // The cursor has been initialized to the first delta.
- assert.isOk(cursor.diffRow);
-
- let firstDeltaRow = diffElement.shadowRoot
- .querySelector('.section.delta .diff-row');
- assert.equal(cursor.diffRow, firstDeltaRow);
-
- firstDeltaRow = diffElement.shadowRoot
- .querySelector('.section.delta .diff-row');
- assert.equal(cursor.diffRow, firstDeltaRow);
-
- cursor.moveDown();
-
- assert.notEqual(cursor.diffRow, firstDeltaRow);
- assert.equal(cursor.diffRow, firstDeltaRow.nextSibling);
-
- cursor.moveUp();
-
- assert.notEqual(cursor.diffRow, firstDeltaRow.nextSibling);
- assert.equal(cursor.diffRow, firstDeltaRow);
- });
- });
-
- test('cursor side functionality', () => {
- // The side only applies to side-by-side mode, which should be the default
- // mode.
- assert.equal(diffElement.viewMode, 'SIDE_BY_SIDE');
-
- const firstDeltaSection = diffElement.shadowRoot
- .querySelector('.section.delta');
- const firstDeltaRow = firstDeltaSection.querySelector('.diff-row');
-
- // Because the first delta in this diff is on the right, it should be set
- // to the right side.
- assert.equal(cursor.side, 'right');
- assert.equal(cursor.diffRow, firstDeltaRow);
- const firstIndex = cursor.cursorManager.index;
-
- // Move the side to the left. Because this delta only has a right side, we
- // should be moved up to the previous line where there is content on the
- // right. The previous row is part of the previous section.
- cursor.moveLeft();
-
- assert.equal(cursor.side, 'left');
- assert.notEqual(cursor.diffRow, firstDeltaRow);
- assert.equal(cursor.cursorManager.index, firstIndex - 1);
- assert.equal(cursor.diffRow.parentElement,
- firstDeltaSection.previousSibling);
-
- // If we move down, we should skip everything in the first delta because
- // we are on the left side and the first delta has no content on the left.
- cursor.moveDown();
-
- assert.equal(cursor.side, 'left');
- assert.notEqual(cursor.diffRow, firstDeltaRow);
- assert.isTrue(cursor.cursorManager.index > firstIndex);
- assert.equal(cursor.diffRow.parentElement,
- firstDeltaSection.nextSibling);
- });
-
- test('chunk skip functionality', () => {
- const chunks = diffElement.root.querySelectorAll(
- '.section.delta');
- const indexOfChunk = function(chunk) {
- return Array.prototype.indexOf.call(chunks, chunk);
- };
-
- // We should be initialized to the first chunk. Since this chunk only has
- // content on the right side, our side should be right.
- let currentIndex = indexOfChunk(cursor.diffRow.parentElement);
- assert.equal(currentIndex, 0);
- assert.equal(cursor.side, 'right');
-
- // Move to the next chunk.
- cursor.moveToNextChunk();
-
- // Since this chunk only has content on the left side. we should have been
- // automatically moved over.
- const previousIndex = currentIndex;
- currentIndex = indexOfChunk(cursor.diffRow.parentElement);
- assert.equal(currentIndex, previousIndex + 1);
- assert.equal(cursor.side, 'left');
- });
-
- suite('moved chunks without line range)', () => {
- setup(async () => {
- const promise = mockPromise();
- const renderHandler = function() {
- diffElement.removeEventListener('render', renderHandler);
- cursor.reInitCursor();
- promise.resolve();
- };
- diffElement.addEventListener('render', renderHandler);
- diffElement.diff = {...diff, content: [
- {
- ab: [
- 'Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, ',
- ],
- },
- {
- b: [
- 'Nullam neque, ligula ac, id blandit.',
- 'Sagittis tincidunt torquent, tempor nunc amet.',
- 'At rhoncus id.',
- ],
- move_details: {changed: false},
- },
- {
- ab: [
- 'Sem nascetur, erat ut, non in.',
- ],
- },
- {
- a: [
- 'Nullam neque, ligula ac, id blandit.',
- 'Sagittis tincidunt torquent, tempor nunc amet.',
- 'At rhoncus id.',
- ],
- move_details: {changed: false},
- },
- {
- ab: [
- 'Arcu eget, rhoncus amet cursus, ipsum elementum.',
- ],
- },
- ]};
- await promise;
- });
-
- test('renders moveControls with simple descriptions', () => {
- const [movedIn, movedOut] = diffElement.root
- .querySelectorAll('.dueToMove .moveControls');
- assert.equal(movedIn.textContent, 'Moved in');
- assert.equal(movedOut.textContent, 'Moved out');
- });
- });
-
- suite('moved chunks (moveDetails)', () => {
- setup(async () => {
- const promise = mockPromise();
- const renderHandler = function() {
- diffElement.removeEventListener('render', renderHandler);
- cursor.reInitCursor();
- promise.resolve();
- };
- diffElement.addEventListener('render', renderHandler);
- diffElement.diff = {...diff, content: [
- {
- ab: [
- 'Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, ',
- ],
- },
- {
- b: [
- 'Nullam neque, ligula ac, id blandit.',
- 'Sagittis tincidunt torquent, tempor nunc amet.',
- 'At rhoncus id.',
- ],
- move_details: {changed: false, range: {start: 4, end: 6}},
- },
- {
- ab: [
- 'Sem nascetur, erat ut, non in.',
- ],
- },
- {
- a: [
- 'Nullam neque, ligula ac, id blandit.',
- 'Sagittis tincidunt torquent, tempor nunc amet.',
- 'At rhoncus id.',
- ],
- move_details: {changed: false, range: {start: 2, end: 4}},
- },
- {
- ab: [
- 'Arcu eget, rhoncus amet cursus, ipsum elementum.',
- ],
- },
- ]};
- await promise;
- });
-
- test('renders moveControls with simple descriptions', () => {
- const [movedIn, movedOut] = diffElement.root
- .querySelectorAll('.dueToMove .moveControls');
- assert.equal(movedIn.textContent, 'Moved from lines 4 - 6');
- assert.equal(movedOut.textContent, 'Moved to lines 2 - 4');
- });
-
- test('startLineAnchor of movedIn chunk fires events', async () => {
- const [movedIn] = diffElement.root
- .querySelectorAll('.dueToMove .moveControls');
- const [startLineAnchor] = movedIn.querySelectorAll('a');
-
- const promise = mockPromise();
- const onMovedLinkClicked = e => {
- assert.deepEqual(e.detail, {lineNum: 4, side: 'left'});
- promise.resolve();
- };
- assert.equal(startLineAnchor.textContent, '4');
- startLineAnchor
- .addEventListener('moved-link-clicked', onMovedLinkClicked);
- MockInteractions.click(startLineAnchor);
- await promise;
- });
-
- test('endLineAnchor of movedOut fires events', async () => {
- const [, movedOut] = diffElement.root
- .querySelectorAll('.dueToMove .moveControls');
- const [, endLineAnchor] = movedOut.querySelectorAll('a');
-
- const promise = mockPromise();
- const onMovedLinkClicked = e => {
- assert.deepEqual(e.detail, {lineNum: 4, side: 'right'});
- promise.resolve();
- };
- assert.equal(endLineAnchor.textContent, '4');
- endLineAnchor.addEventListener('moved-link-clicked', onMovedLinkClicked);
- MockInteractions.click(endLineAnchor);
- await promise;
- });
- });
-
- test('initialLineNumber not provided', async () => {
- let scrollBehaviorDuringMove;
- const moveToNumStub = sinon.stub(cursor, 'moveToLineNumber');
- const moveToChunkStub = sinon.stub(cursor, 'moveToFirstChunk')
- .callsFake(() => {
- scrollBehaviorDuringMove = cursor.cursorManager.scrollMode;
- });
-
- diffElement._diffChanged(createDiff());
- await new Promise(resolve => afterNextRender(diffElement, resolve));
- cursor.reInitCursor();
- assert.isFalse(moveToNumStub.called);
- assert.isTrue(moveToChunkStub.called);
- assert.equal(scrollBehaviorDuringMove, 'never');
- assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
- });
-
- test('initialLineNumber provided', async () => {
- let scrollBehaviorDuringMove;
- const moveToNumStub = sinon.stub(cursor, 'moveToLineNumber')
- .callsFake(() => {
- scrollBehaviorDuringMove = cursor.cursorManager.scrollMode;
- });
- const moveToChunkStub = sinon.stub(cursor, 'moveToFirstChunk');
- cursor.initialLineNumber = 10;
- cursor.side = 'right';
-
- diffElement._diffChanged(createDiff());
- await new Promise(resolve => afterNextRender(diffElement, resolve));
- cursor.reInitCursor();
- assert.isFalse(moveToChunkStub.called);
- assert.isTrue(moveToNumStub.called);
- assert.equal(moveToNumStub.lastCall.args[0], 10);
- assert.equal(moveToNumStub.lastCall.args[1], 'right');
- assert.equal(scrollBehaviorDuringMove, 'keep-visible');
- assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
- });
-
- test('getTargetDiffElement', () => {
- cursor.initialLineNumber = 1;
- assert.isTrue(!!cursor.diffRow);
- assert.equal(
- cursor.getTargetDiffElement(),
- diffElement
- );
- });
-
- suite('createCommentInPlace', () => {
- setup(() => {
- diffElement.loggedIn = true;
- });
-
- test('adds new draft for selected line on the left', async () => {
- cursor.moveToLineNumber(2, 'left');
- const promise = mockPromise();
- diffElement.addEventListener('create-comment', e => {
- const {lineNum, range, side} = e.detail;
- assert.equal(lineNum, 2);
- assert.equal(range, undefined);
- assert.equal(side, 'left');
- promise.resolve();
- });
- cursor.createCommentInPlace();
- await promise;
- });
-
- test('adds draft for selected line on the right', async () => {
- cursor.moveToLineNumber(4, 'right');
- const promise = mockPromise();
- diffElement.addEventListener('create-comment', e => {
- const {lineNum, range, side} = e.detail;
- assert.equal(lineNum, 4);
- assert.equal(range, undefined);
- assert.equal(side, 'right');
- promise.resolve();
- });
- cursor.createCommentInPlace();
- await promise;
- });
-
- test('creates comment for range if selected', async () => {
- const someRange = {
- start_line: 2,
- start_character: 3,
- end_line: 6,
- end_character: 1,
- };
- diffElement.highlights.selectedRange = {
- side: 'right',
- range: someRange,
- };
- const promise = mockPromise();
- diffElement.addEventListener('create-comment', e => {
- const {lineNum, range, side} = e.detail;
- assert.equal(lineNum, 6);
- assert.equal(range, someRange);
- assert.equal(side, 'right');
- promise.resolve();
- });
- cursor.createCommentInPlace();
- await promise;
- });
-
- test('ignores call if nothing is selected', () => {
- const createRangeCommentStub = sinon.stub(diffElement,
- 'createRangeComment');
- const addDraftAtLineStub = sinon.stub(diffElement, 'addDraftAtLine');
- cursor.diffRow = undefined;
- cursor.createCommentInPlace();
- assert.isFalse(createRangeCommentStub.called);
- assert.isFalse(addDraftAtLineStub.called);
- });
- });
-
- test('getAddress', () => {
- // It should initialize to the first chunk: line 5 of the revision.
- assert.deepEqual(cursor.getAddress(),
- {leftSide: false, number: 5});
-
- // Revision line 4 is up.
- cursor.moveUp();
- assert.deepEqual(cursor.getAddress(),
- {leftSide: false, number: 4});
-
- // Base line 4 is left.
- cursor.moveLeft();
- assert.deepEqual(cursor.getAddress(), {leftSide: true, number: 4});
-
- // Moving to the next chunk takes it back to the start.
- cursor.moveToNextChunk();
- assert.deepEqual(cursor.getAddress(),
- {leftSide: false, number: 5});
-
- // The following chunk is a removal starting on line 10 of the base.
- cursor.moveToNextChunk();
- assert.deepEqual(cursor.getAddress(),
- {leftSide: true, number: 10});
-
- // Should be null if there is no selection.
- cursor.cursorManager.unsetCursor();
- assert.isNotOk(cursor.getAddress());
- });
-
- test('_findRowByNumberAndFile', () => {
- // Get the first ab row after the first chunk.
- const row = diffElement.root.querySelectorAll('tr')[9];
-
- // It should be line 8 on the right, but line 5 on the left.
- assert.equal(cursor._findRowByNumberAndFile(8, 'right'), row);
- assert.equal(cursor._findRowByNumberAndFile(5, 'left'), row);
- });
-
- test('expand context updates stops', async () => {
- sinon.spy(cursor, '_updateStops');
- MockInteractions.tap(diffElement.shadowRoot
- .querySelector('gr-context-controls').shadowRoot
- .querySelector('.showContext'));
- await new Promise(resolve => afterNextRender(diffElement, resolve));
- assert.isTrue(cursor._updateStops.called);
- });
-
- test('updates stops when loading changes', () => {
- sinon.spy(cursor, '_updateStops');
- diffElement.dispatchEvent(new Event('loading-changed'));
- assert.isTrue(cursor._updateStops.called);
- });
-
- suite('multi diff', () => {
- let diffElements;
-
- setup(async () => {
- diffElements = [
- await fixture(html`<gr-diff></gr-diff>`),
- await fixture(html`<gr-diff></gr-diff>`),
- await fixture(html`<gr-diff></gr-diff>`),
- ];
- cursor = new GrDiffCursor();
-
- // Register the diff with the cursor.
- cursor.replaceDiffs(diffElements);
-
- for (const el of diffElements) {
- el.prefs = createDefaultDiffPrefs();
- }
- });
-
- function getTargetDiffIndex() {
- // Mocha has a bug where when `assert.equals` fails, it will try to
- // JSON.stringify the operands, which fails when they are cyclic structures
- // like GrDiffElement. The failure is difficult to attribute to a specific
- // assertion because of the async nature assertion errors are handled and
- // can cause the test simply timing out, causing a lot of debugging headache.
- // Working with indices circumvents the problem.
- return diffElements.indexOf(cursor.getTargetDiffElement());
- }
-
- test('do not skip loading diffs', async () => {
- const diffRenderedPromises =
- diffElements.map(diffEl => listenOnce(diffEl, 'render'));
-
- diffElements[0].diff = createDiff();
- diffElements[2].diff = createDiff();
- await Promise.all([diffRenderedPromises[0], diffRenderedPromises[2]]);
- await new Promise(resolve => afterNextRender(diffElements[0], resolve));
-
- const lastLine = diffElements[0].diff.meta_b.lines;
-
- // Goto second last line of the first diff
- cursor.moveToLineNumber(lastLine - 1, 'right');
- assert.equal(
- cursor.getTargetLineElement().textContent, lastLine - 1);
-
- // Can move down until we reach the loading file
- cursor.moveDown();
- assert.equal(getTargetDiffIndex(), 0);
- assert.equal(cursor.getTargetLineElement().textContent, lastLine);
-
- // Cannot move down while still loading the diff we would switch to
- cursor.moveDown();
- assert.equal(getTargetDiffIndex(), 0);
- assert.equal(cursor.getTargetLineElement().textContent, lastLine);
-
- // Diff 1 finishing to load
- diffElements[1].diff = createDiff();
- await diffRenderedPromises[1];
- await new Promise(resolve => afterNextRender(diffElements[0], resolve));
-
- // Now we can go down
- cursor.moveDown();
- assert.equal(getTargetDiffIndex(), 1);
- assert.equal(cursor.getTargetLineElement().textContent, 'File');
- });
- });
-});
-
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.ts
new file mode 100644
index 0000000..ac9b407
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-cursor/gr-diff-cursor_test.ts
@@ -0,0 +1,693 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup-karma';
+import '../gr-diff/gr-diff';
+import './gr-diff-cursor';
+import {fixture, html} from '@open-wc/testing-helpers';
+import {mockPromise, queryAll, queryAndAssert} from '../../../test/test-utils';
+import {createDiff} from '../../../test/test-data-generators';
+import {createDefaultDiffPrefs} from '../../../constants/constants';
+import {GrDiffCursor} from './gr-diff-cursor';
+import {waitForEventOnce} from '../../../utils/event-util';
+import {DiffInfo, DiffViewMode, Side} from '../../../api/diff';
+import {GrDiff} from '../gr-diff/gr-diff';
+import {assertIsDefined} from '../../../utils/common-util';
+
+suite('gr-diff-cursor tests', () => {
+ let cursor: GrDiffCursor;
+ let diffElement: GrDiff;
+ let diff: DiffInfo;
+
+ setup(async () => {
+ diffElement = await fixture(html`<gr-diff></gr-diff>`);
+ cursor = new GrDiffCursor();
+
+ // Register the diff with the cursor.
+ cursor.replaceDiffs([diffElement]);
+
+ diffElement.loggedIn = false;
+ diffElement.path = 'some/path.ts';
+ const promise = mockPromise();
+ const setupDone = () => {
+ cursor._updateStops();
+ cursor.moveToFirstChunk();
+ diffElement.removeEventListener('render', setupDone);
+ promise.resolve();
+ };
+ diffElement.addEventListener('render', setupDone);
+
+ diff = createDiff();
+ diffElement.prefs = createDefaultDiffPrefs();
+ diffElement.diff = diff;
+ await promise;
+ });
+
+ test('diff cursor functionality (side-by-side)', () => {
+ // The cursor has been initialized to the first delta.
+ assert.isOk(cursor.diffRow);
+
+ const firstDeltaRow = queryAndAssert<HTMLElement>(
+ diffElement,
+ '.section.delta .diff-row'
+ );
+ assert.equal(cursor.diffRow, firstDeltaRow);
+
+ cursor.moveDown();
+
+ assert.isOk(firstDeltaRow.nextElementSibling);
+ assert.notEqual(cursor.diffRow, firstDeltaRow);
+ assert.equal(
+ cursor.diffRow,
+ firstDeltaRow.nextElementSibling as HTMLElement
+ );
+
+ cursor.moveUp();
+
+ assert.isOk(firstDeltaRow.nextElementSibling);
+ assert.notEqual(
+ cursor.diffRow,
+ firstDeltaRow.nextElementSibling as HTMLElement
+ );
+ assert.equal(cursor.diffRow, firstDeltaRow);
+ });
+
+ test('moveToFirstChunk', async () => {
+ const diff: DiffInfo = {
+ meta_a: {
+ name: 'lorem-ipsum.txt',
+ content_type: 'text/plain',
+ lines: 3,
+ },
+ meta_b: {
+ name: 'lorem-ipsum.txt',
+ content_type: 'text/plain',
+ lines: 3,
+ },
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/lorem-ipsum.txt b/lorem-ipsum.txt',
+ 'index b2adcf4..554ae49 100644',
+ '--- a/lorem-ipsum.txt',
+ '+++ b/lorem-ipsum.txt',
+ ],
+ content: [
+ {b: ['new line 1']},
+ {ab: ['unchanged line']},
+ {a: ['old line 2']},
+ {ab: ['more unchanged lines']},
+ ],
+ };
+
+ diffElement.diff = diff;
+ // The file comment button, if present, is a cursor stop. Ensure
+ // moveToFirstChunk() works correctly even if the button is not shown.
+ diffElement.prefs!.show_file_comment_button = false;
+ await waitForEventOnce(diffElement, 'render');
+
+ cursor._updateStops();
+
+ const chunks = [
+ ...queryAll(diffElement, '.section.delta'),
+ ] as HTMLElement[];
+ assert.equal(chunks.length, 2);
+
+ // Verify it works on fresh diff.
+ cursor.moveToFirstChunk();
+ assert.ok(cursor.diffRow);
+ assert.equal(chunks.indexOf(cursor.diffRow!.parentElement!), 0);
+ assert.equal(cursor.side, Side.RIGHT);
+
+ // Verify it works from other cursor positions.
+ cursor.moveToNextChunk();
+ assert.ok(cursor.diffRow);
+ assert.equal(chunks.indexOf(cursor.diffRow!.parentElement!), 1);
+ assert.equal(cursor.side, Side.LEFT);
+ cursor.moveToFirstChunk();
+ assert.ok(cursor.diffRow);
+ assert.equal(chunks.indexOf(cursor.diffRow!.parentElement!), 0);
+ assert.equal(cursor.side, Side.RIGHT);
+ });
+
+ test('moveToLastChunk', async () => {
+ const diff: DiffInfo = {
+ meta_a: {
+ name: 'lorem-ipsum.txt',
+ content_type: 'text/plain',
+ lines: 3,
+ },
+ meta_b: {
+ name: 'lorem-ipsum.txt',
+ content_type: 'text/plain',
+ lines: 3,
+ },
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/lorem-ipsum.txt b/lorem-ipsum.txt',
+ 'index b2adcf4..554ae49 100644',
+ '--- a/lorem-ipsum.txt',
+ '+++ b/lorem-ipsum.txt',
+ ],
+ content: [
+ {ab: ['unchanged line']},
+ {a: ['old line 2']},
+ {ab: ['more unchanged lines']},
+ {b: ['new line 3']},
+ ],
+ };
+
+ diffElement.diff = diff;
+ await waitForEventOnce(diffElement, 'render');
+ cursor._updateStops();
+
+ const chunks = [...queryAll(diffElement, '.section.delta')];
+ assert.equal(chunks.length, 2);
+
+ // Verify it works on fresh diff.
+ cursor.moveToLastChunk();
+ assert.equal(chunks.indexOf(cursor.diffRow!.parentElement!), 1);
+ assert.equal(cursor.side, Side.RIGHT);
+
+ // Verify it works from other cursor positions.
+ cursor.moveToPreviousChunk();
+ assert.equal(chunks.indexOf(cursor.diffRow!.parentElement!), 0);
+ assert.equal(cursor.side, Side.LEFT);
+ cursor.moveToLastChunk();
+ assert.equal(chunks.indexOf(cursor.diffRow!.parentElement!), 1);
+ assert.equal(cursor.side, Side.RIGHT);
+ });
+
+ test('cursor scroll behavior', () => {
+ assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
+
+ diffElement.dispatchEvent(new Event('render-start'));
+ assert.isTrue(cursor.cursorManager.focusOnMove);
+
+ window.dispatchEvent(new Event('scroll'));
+ assert.equal(cursor.cursorManager.scrollMode, 'never');
+ assert.isFalse(cursor.cursorManager.focusOnMove);
+
+ diffElement.dispatchEvent(new Event('render-content'));
+ assert.isTrue(cursor.cursorManager.focusOnMove);
+
+ cursor.reInitCursor();
+ assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
+ });
+
+ test('moves to selected line', () => {
+ const moveToNumStub = sinon.stub(cursor, 'moveToLineNumber');
+
+ diffElement.dispatchEvent(
+ new CustomEvent('line-selected', {
+ detail: {number: '123', side: Side.RIGHT, path: 'some/file'},
+ })
+ );
+
+ assert.isTrue(moveToNumStub.called);
+ assert.equal(moveToNumStub.lastCall.args[0], 123);
+ assert.equal(moveToNumStub.lastCall.args[1], Side.RIGHT);
+ assert.equal(moveToNumStub.lastCall.args[2], 'some/file');
+ });
+
+ suite('unified diff', () => {
+ setup(async () => {
+ diffElement.viewMode = DiffViewMode.UNIFIED;
+ await waitForEventOnce(diffElement, 'render');
+ cursor.reInitCursor();
+ });
+
+ test('diff cursor functionality (unified)', () => {
+ // The cursor has been initialized to the first delta.
+ assert.isOk(cursor.diffRow);
+
+ const firstDeltaRow = queryAndAssert<HTMLElement>(
+ diffElement,
+ '.section.delta .diff-row'
+ );
+ assert.equal(cursor.diffRow, firstDeltaRow);
+
+ cursor.moveDown();
+
+ assert.notEqual(cursor.diffRow, firstDeltaRow);
+ assert.equal(
+ cursor.diffRow,
+ firstDeltaRow.nextElementSibling as HTMLElement
+ );
+
+ cursor.moveUp();
+
+ assert.notEqual(
+ cursor.diffRow,
+ firstDeltaRow.nextElementSibling as HTMLElement
+ );
+ assert.equal(cursor.diffRow, firstDeltaRow);
+ });
+ });
+
+ test('cursor side functionality', () => {
+ // The side only applies to side-by-side mode, which should be the default
+ // mode.
+ assert.equal(diffElement.viewMode, 'SIDE_BY_SIDE');
+
+ const firstDeltaSection = queryAndAssert<HTMLElement>(
+ diffElement,
+ '.section.delta'
+ );
+ const firstDeltaRow = queryAndAssert<HTMLElement>(
+ firstDeltaSection,
+ '.diff-row'
+ );
+
+ // Because the first delta in this diff is on the right, it should be set
+ // to the right side.
+ assert.equal(cursor.side, Side.RIGHT);
+ assert.equal(cursor.diffRow, firstDeltaRow);
+ const firstIndex = cursor.cursorManager.index;
+
+ // Move the side to the left. Because this delta only has a right side, we
+ // should be moved up to the previous line where there is content on the
+ // right. The previous row is part of the previous section.
+ cursor.moveLeft();
+
+ assert.equal(cursor.side, Side.LEFT);
+ assert.notEqual(cursor.diffRow, firstDeltaRow);
+ assert.equal(cursor.cursorManager.index, firstIndex - 1);
+ assert.equal(
+ cursor.diffRow!.parentElement,
+ firstDeltaSection.previousSibling
+ );
+
+ // If we move down, we should skip everything in the first delta because
+ // we are on the left side and the first delta has no content on the left.
+ cursor.moveDown();
+
+ assert.equal(cursor.side, Side.LEFT);
+ assert.notEqual(cursor.diffRow, firstDeltaRow);
+ assert.isTrue(cursor.cursorManager.index > firstIndex);
+ assert.equal(cursor.diffRow!.parentElement, firstDeltaSection.nextSibling);
+ });
+
+ test('chunk skip functionality', () => {
+ const chunks = [...queryAll(diffElement, '.section.delta')];
+ const indexOfChunk = function (chunk: HTMLElement) {
+ return Array.prototype.indexOf.call(chunks, chunk);
+ };
+
+ // We should be initialized to the first chunk. Since this chunk only has
+ // content on the right side, our side should be right.
+ let currentIndex = indexOfChunk(cursor.diffRow!.parentElement!);
+ assert.equal(currentIndex, 0);
+ assert.equal(cursor.side, Side.RIGHT);
+
+ // Move to the next chunk.
+ cursor.moveToNextChunk();
+
+ // Since this chunk only has content on the left side. we should have been
+ // automatically moved over.
+ const previousIndex = currentIndex;
+ currentIndex = indexOfChunk(cursor.diffRow!.parentElement!);
+ assert.equal(currentIndex, previousIndex + 1);
+ assert.equal(cursor.side, Side.LEFT);
+ });
+
+ suite('moved chunks without line range)', () => {
+ setup(async () => {
+ const promise = mockPromise();
+ const renderHandler = function () {
+ diffElement.removeEventListener('render', renderHandler);
+ cursor.reInitCursor();
+ promise.resolve();
+ };
+ diffElement.addEventListener('render', renderHandler);
+ diffElement.diff = {
+ ...diff,
+ content: [
+ {
+ ab: ['Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, '],
+ },
+ {
+ b: [
+ 'Nullam neque, ligula ac, id blandit.',
+ 'Sagittis tincidunt torquent, tempor nunc amet.',
+ 'At rhoncus id.',
+ ],
+ move_details: {changed: false},
+ },
+ {
+ ab: ['Sem nascetur, erat ut, non in.'],
+ },
+ {
+ a: [
+ 'Nullam neque, ligula ac, id blandit.',
+ 'Sagittis tincidunt torquent, tempor nunc amet.',
+ 'At rhoncus id.',
+ ],
+ move_details: {changed: false},
+ },
+ {
+ ab: ['Arcu eget, rhoncus amet cursus, ipsum elementum.'],
+ },
+ ],
+ };
+ await promise;
+ });
+
+ test('renders moveControls with simple descriptions', () => {
+ const [movedIn, movedOut] = [
+ ...queryAll(diffElement, '.dueToMove .moveControls'),
+ ];
+ assert.equal(movedIn.textContent, 'Moved in');
+ assert.equal(movedOut.textContent, 'Moved out');
+ });
+ });
+
+ suite('moved chunks (moveDetails)', () => {
+ setup(async () => {
+ const promise = mockPromise();
+ const renderHandler = function () {
+ diffElement.removeEventListener('render', renderHandler);
+ cursor.reInitCursor();
+ promise.resolve();
+ };
+ diffElement.addEventListener('render', renderHandler);
+ diffElement.diff = {
+ ...diff,
+ content: [
+ {
+ ab: ['Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, '],
+ },
+ {
+ b: [
+ 'Nullam neque, ligula ac, id blandit.',
+ 'Sagittis tincidunt torquent, tempor nunc amet.',
+ 'At rhoncus id.',
+ ],
+ move_details: {changed: false, range: {start: 4, end: 6}},
+ },
+ {
+ ab: ['Sem nascetur, erat ut, non in.'],
+ },
+ {
+ a: [
+ 'Nullam neque, ligula ac, id blandit.',
+ 'Sagittis tincidunt torquent, tempor nunc amet.',
+ 'At rhoncus id.',
+ ],
+ move_details: {changed: false, range: {start: 2, end: 4}},
+ },
+ {
+ ab: ['Arcu eget, rhoncus amet cursus, ipsum elementum.'],
+ },
+ ],
+ };
+ await promise;
+ });
+
+ test('renders moveControls with simple descriptions', () => {
+ const [movedIn, movedOut] = [
+ ...queryAll(diffElement, '.dueToMove .moveControls'),
+ ];
+ assert.equal(movedIn.textContent, 'Moved from lines 4 - 6');
+ assert.equal(movedOut.textContent, 'Moved to lines 2 - 4');
+ });
+
+ test('startLineAnchor of movedIn chunk fires events', async () => {
+ const [movedIn] = [...queryAll(diffElement, '.dueToMove .moveControls')];
+ const [startLineAnchor] = movedIn.querySelectorAll('a');
+
+ const promise = mockPromise();
+ const onMovedLinkClicked = (e: CustomEvent) => {
+ assert.deepEqual(e.detail, {lineNum: 4, side: Side.LEFT});
+ promise.resolve();
+ };
+ assert.equal(startLineAnchor.textContent, '4');
+ startLineAnchor.addEventListener(
+ 'moved-link-clicked',
+ onMovedLinkClicked
+ );
+ startLineAnchor.click();
+ await promise;
+ });
+
+ test('endLineAnchor of movedOut fires events', async () => {
+ const [, movedOut] = [
+ ...queryAll(diffElement, '.dueToMove .moveControls'),
+ ];
+ const [, endLineAnchor] = movedOut.querySelectorAll('a');
+
+ const promise = mockPromise();
+ const onMovedLinkClicked = (e: CustomEvent) => {
+ assert.deepEqual(e.detail, {lineNum: 4, side: Side.RIGHT});
+ promise.resolve();
+ };
+ assert.equal(endLineAnchor.textContent, '4');
+ endLineAnchor.addEventListener('moved-link-clicked', onMovedLinkClicked);
+ endLineAnchor.click();
+ await promise;
+ });
+ });
+
+ test('initialLineNumber not provided', async () => {
+ let scrollBehaviorDuringMove;
+ const moveToNumStub = sinon.stub(cursor, 'moveToLineNumber');
+ const moveToChunkStub = sinon
+ .stub(cursor, 'moveToFirstChunk')
+ .callsFake(() => {
+ scrollBehaviorDuringMove = cursor.cursorManager.scrollMode;
+ });
+
+ diffElement._diffChanged(createDiff());
+ await waitForEventOnce(diffElement, 'render');
+ cursor.reInitCursor();
+ assert.isFalse(moveToNumStub.called);
+ assert.isTrue(moveToChunkStub.called);
+ assert.equal(scrollBehaviorDuringMove, 'never');
+ assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
+ });
+
+ test('initialLineNumber provided', async () => {
+ let scrollBehaviorDuringMove;
+ const moveToNumStub = sinon
+ .stub(cursor, 'moveToLineNumber')
+ .callsFake(() => {
+ scrollBehaviorDuringMove = cursor.cursorManager.scrollMode;
+ });
+ const moveToChunkStub = sinon.stub(cursor, 'moveToFirstChunk');
+ cursor.initialLineNumber = 10;
+ cursor.side = Side.RIGHT;
+
+ diffElement._diffChanged(createDiff());
+ await waitForEventOnce(diffElement, 'render');
+ cursor.reInitCursor();
+ assert.isFalse(moveToChunkStub.called);
+ assert.isTrue(moveToNumStub.called);
+ assert.equal(moveToNumStub.lastCall.args[0], 10);
+ assert.equal(moveToNumStub.lastCall.args[1], Side.RIGHT);
+ assert.equal(scrollBehaviorDuringMove, 'keep-visible');
+ assert.equal(cursor.cursorManager.scrollMode, 'keep-visible');
+ });
+
+ test('getTargetDiffElement', () => {
+ cursor.initialLineNumber = 1;
+ assert.isTrue(!!cursor.diffRow);
+ assert.equal(cursor.getTargetDiffElement(), diffElement);
+ });
+
+ suite('createCommentInPlace', () => {
+ setup(() => {
+ diffElement.loggedIn = true;
+ });
+
+ test('adds new draft for selected line on the left', async () => {
+ cursor.moveToLineNumber(2, Side.LEFT);
+ const promise = mockPromise();
+ diffElement.addEventListener('create-comment', e => {
+ const {lineNum, range, side} = e.detail;
+ assert.equal(lineNum, 2);
+ assert.equal(range, undefined);
+ assert.equal(side, Side.LEFT);
+ promise.resolve();
+ });
+ cursor.createCommentInPlace();
+ await promise;
+ });
+
+ test('adds draft for selected line on the right', async () => {
+ cursor.moveToLineNumber(4, Side.RIGHT);
+ const promise = mockPromise();
+ diffElement.addEventListener('create-comment', e => {
+ const {lineNum, range, side} = e.detail;
+ assert.equal(lineNum, 4);
+ assert.equal(range, undefined);
+ assert.equal(side, Side.RIGHT);
+ promise.resolve();
+ });
+ cursor.createCommentInPlace();
+ await promise;
+ });
+
+ test('creates comment for range if selected', async () => {
+ const someRange = {
+ start_line: 2,
+ start_character: 3,
+ end_line: 6,
+ end_character: 1,
+ };
+ diffElement.highlights.selectedRange = {
+ side: Side.RIGHT,
+ range: someRange,
+ };
+ const promise = mockPromise();
+ diffElement.addEventListener('create-comment', e => {
+ const {lineNum, range, side} = e.detail;
+ assert.equal(lineNum, 6);
+ assert.equal(range, someRange);
+ assert.equal(side, Side.RIGHT);
+ promise.resolve();
+ });
+ cursor.createCommentInPlace();
+ await promise;
+ });
+
+ test('ignores call if nothing is selected', () => {
+ const createRangeCommentStub = sinon.stub(
+ diffElement,
+ 'createRangeComment'
+ );
+ const addDraftAtLineStub = sinon.stub(diffElement, 'addDraftAtLine');
+ cursor.diffRow = undefined;
+ cursor.createCommentInPlace();
+ assert.isFalse(createRangeCommentStub.called);
+ assert.isFalse(addDraftAtLineStub.called);
+ });
+ });
+
+ test('getAddress', () => {
+ // It should initialize to the first chunk: line 5 of the revision.
+ assert.deepEqual(cursor.getAddress(), {leftSide: false, number: 5});
+
+ // Revision line 4 is up.
+ cursor.moveUp();
+ assert.deepEqual(cursor.getAddress(), {leftSide: false, number: 4});
+
+ // Base line 4 is left.
+ cursor.moveLeft();
+ assert.deepEqual(cursor.getAddress(), {leftSide: true, number: 4});
+
+ // Moving to the next chunk takes it back to the start.
+ cursor.moveToNextChunk();
+ assert.deepEqual(cursor.getAddress(), {leftSide: false, number: 5});
+
+ // The following chunk is a removal starting on line 10 of the base.
+ cursor.moveToNextChunk();
+ assert.deepEqual(cursor.getAddress(), {leftSide: true, number: 10});
+
+ // Should be null if there is no selection.
+ cursor.cursorManager.unsetCursor();
+ assert.isNotOk(cursor.getAddress());
+ });
+
+ test('_findRowByNumberAndFile', () => {
+ // Get the first ab row after the first chunk.
+ const rows = [...queryAll<HTMLTableRowElement>(diffElement, 'tr')];
+ const row = rows[9];
+ assert.ok(row);
+
+ // It should be line 8 on the right, but line 5 on the left.
+ assert.equal(cursor._findRowByNumberAndFile(8, Side.RIGHT), row);
+ assert.equal(cursor._findRowByNumberAndFile(5, Side.LEFT), row);
+ });
+
+ test('expand context updates stops', async () => {
+ const spy = sinon.spy(cursor, '_updateStops');
+ const controls = queryAndAssert(diffElement, 'gr-context-controls');
+ const showContext = queryAndAssert<HTMLElement>(controls, '.showContext');
+ showContext.click();
+ await waitForEventOnce(diffElement, 'render');
+ assert.isTrue(spy.called);
+ });
+
+ test('updates stops when loading changes', () => {
+ const spy = sinon.spy(cursor, '_updateStops');
+ diffElement.dispatchEvent(new Event('loading-changed'));
+ assert.isTrue(spy.called);
+ });
+
+ suite('multi diff', () => {
+ let diffElements: GrDiff[];
+
+ setup(async () => {
+ diffElements = [
+ await fixture(html`<gr-diff></gr-diff>`),
+ await fixture(html`<gr-diff></gr-diff>`),
+ await fixture(html`<gr-diff></gr-diff>`),
+ ];
+ cursor = new GrDiffCursor();
+
+ // Register the diff with the cursor.
+ cursor.replaceDiffs(diffElements);
+
+ for (const el of diffElements) {
+ el.prefs = createDefaultDiffPrefs();
+ }
+ });
+
+ function getTargetDiffIndex() {
+ // Mocha has a bug where when `assert.equals` fails, it will try to
+ // JSON.stringify the operands, which fails when they are cyclic structures
+ // like GrDiffElement. The failure is difficult to attribute to a specific
+ // assertion because of the async nature assertion errors are handled and
+ // can cause the test simply timing out, causing a lot of debugging headache.
+ // Working with indices circumvents the problem.
+ const target = cursor.getTargetDiffElement();
+ assertIsDefined(target);
+ return diffElements.indexOf(target);
+ }
+
+ test('do not skip loading diffs', async () => {
+ diffElements[0].diff = createDiff();
+ diffElements[2].diff = createDiff();
+ await waitForEventOnce(diffElements[0], 'render');
+ await waitForEventOnce(diffElements[2], 'render');
+
+ const lastLine = diffElements[0].diff.meta_b?.lines;
+ assertIsDefined(lastLine);
+
+ // Goto second last line of the first diff
+ cursor.moveToLineNumber(lastLine - 1, Side.RIGHT);
+ assert.equal(
+ cursor.getTargetLineElement()!.textContent,
+ `${lastLine - 1}`
+ );
+
+ // Can move down until we reach the loading file
+ cursor.moveDown();
+ assert.equal(getTargetDiffIndex(), 0);
+ assert.equal(
+ cursor.getTargetLineElement()!.textContent,
+ lastLine.toString()
+ );
+
+ // Cannot move down while still loading the diff we would switch to
+ cursor.moveDown();
+ assert.equal(getTargetDiffIndex(), 0);
+ assert.equal(
+ cursor.getTargetLineElement()!.textContent,
+ lastLine.toString()
+ );
+
+ // Diff 1 finishing to load
+ diffElements[1].diff = createDiff();
+ await waitForEventOnce(diffElements[1], 'render');
+
+ // Now we can go down
+ cursor.moveDown();
+ assert.equal(getTargetDiffIndex(), 1);
+ assert.equal(cursor.getTargetLineElement()!.textContent, 'File');
+ });
+ });
+});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
index 6f71c65..cc44f50 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
@@ -138,6 +138,12 @@
return Promise.resolve();
}
+ // TODO: Canceling this promise does not help much. `nextStep` will continue
+ // to be scheduled anyway. So either just remove the cancelable promise, so
+ // future programmers are not fooled about this promise can do. Or fix the
+ // scheduling of `nextStep` such that cancellation is taken into account.
+ // The easiest approach is likely to just not re-use the processor for
+ // multiple processing passes. There is no benefit from doing so.
this.processPromise = makeCancelable(
new Promise(resolve => {
const state = {
@@ -614,6 +620,7 @@
const result = [];
let lastChunkEndOffset = 0;
for (const {offset, keyLocation} of chunkEnds) {
+ if (lastChunkEndOffset === offset) continue;
result.push({
lines: lines.slice(lastChunkEndOffset, offset),
keyLocation,
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
index b8262e0..e015d49 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
@@ -17,6 +17,7 @@
import {BLANK_LINE, GrDiffLine, GrDiffLineType} from './gr-diff-line';
import {LineRange, Side} from '../../../api/diff';
import {LineNumber} from './gr-diff-line';
+import {assertIsDefined, check} from '../../../utils/common-util';
export enum GrDiffGroupType {
/** Unchanged context. */
@@ -275,7 +276,9 @@
},
};
} else {
- for (const line of options.lines ?? []) {
+ assertIsDefined(options.lines);
+ check(options.lines.length > 0, 'diff group must have lines');
+ for (const line of options.lines) {
this.addLine(line);
}
}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.js b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.ts
similarity index 60%
rename from polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.js
rename to polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.ts
index 321086c..bbf03ae 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.js
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.ts
@@ -1,32 +1,25 @@
/**
* @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.
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
-
-import '../../../test/common-test-setup-karma.js';
-import {GrDiffLine, GrDiffLineType, BLANK_LINE} from './gr-diff-line.js';
-import {GrDiffGroup, GrDiffGroupType, hideInContextControl} from './gr-diff-group.js';
+import '../../../test/common-test-setup-karma';
+import {GrDiffLine, GrDiffLineType, BLANK_LINE} from './gr-diff-line';
+import {
+ GrDiffGroup,
+ GrDiffGroupType,
+ hideInContextControl,
+} from './gr-diff-group';
suite('gr-diff-group tests', () => {
test('delta line pairs', () => {
const l1 = new GrDiffLine(GrDiffLineType.ADD, 0, 128);
const l2 = new GrDiffLine(GrDiffLineType.ADD, 0, 129);
const l3 = new GrDiffLine(GrDiffLineType.REMOVE, 64, 0);
- let group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines: [
- l1, l2, 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]);
@@ -53,13 +46,25 @@
]);
});
+ test('group must have lines', () => {
+ try {
+ new GrDiffGroup({type: GrDiffGroupType.BOTH});
+ } catch (e) {
+ // expected
+ return;
+ }
+ assert.fail('a standard diff group cannot be empty');
+ });
+
test('group/header line pairs', () => {
const l1 = new GrDiffLine(GrDiffLineType.BOTH, 64, 128);
const l2 = new GrDiffLine(GrDiffLineType.BOTH, 65, 129);
const l3 = new GrDiffLine(GrDiffLineType.BOTH, 66, 130);
const group = new GrDiffGroup({
- type: GrDiffGroupType.BOTH, lines: [l1, l2, l3]});
+ type: GrDiffGroupType.BOTH,
+ lines: [l1, l2, l3],
+ });
assert.deepEqual(group.lines, [l1, l2, l3]);
assert.deepEqual(group.adds, []);
@@ -83,34 +88,44 @@
const l2 = new GrDiffLine(GrDiffLineType.REMOVE);
const l3 = new GrDiffLine(GrDiffLineType.BOTH);
- assert.throws(() =>
- new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [l1, l2, l3]}));
+ assert.throws(
+ () => new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [l1, l2, l3]})
+ );
});
suite('hideInContextControl', () => {
- let groups;
+ let groups: GrDiffGroup[];
setup(() => {
groups = [
- 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({type: GrDiffGroupType.DELTA, lines: [
- new GrDiffLine(GrDiffLineType.REMOVE, 8),
- new GrDiffLine(GrDiffLineType.ADD, 0, 10),
- new GrDiffLine(GrDiffLineType.REMOVE, 9),
- new GrDiffLine(GrDiffLineType.ADD, 0, 11),
- new GrDiffLine(GrDiffLineType.REMOVE, 10),
- new GrDiffLine(GrDiffLineType.ADD, 0, 12),
- new GrDiffLine(GrDiffLineType.REMOVE, 11),
- new GrDiffLine(GrDiffLineType.ADD, 0, 13),
- ]}),
- new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [
- new GrDiffLine(GrDiffLineType.BOTH, 12, 14),
- new GrDiffLine(GrDiffLineType.BOTH, 13, 15),
- new GrDiffLine(GrDiffLineType.BOTH, 14, 16),
- ]}),
+ 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({
+ type: GrDiffGroupType.DELTA,
+ lines: [
+ new GrDiffLine(GrDiffLineType.REMOVE, 8),
+ new GrDiffLine(GrDiffLineType.ADD, 0, 10),
+ new GrDiffLine(GrDiffLineType.REMOVE, 9),
+ new GrDiffLine(GrDiffLineType.ADD, 0, 11),
+ new GrDiffLine(GrDiffLineType.REMOVE, 10),
+ new GrDiffLine(GrDiffLineType.ADD, 0, 12),
+ new GrDiffLine(GrDiffLineType.REMOVE, 11),
+ new GrDiffLine(GrDiffLineType.ADD, 0, 13),
+ ],
+ }),
+ new GrDiffGroup({
+ type: GrDiffGroupType.BOTH,
+ lines: [
+ new GrDiffLine(GrDiffLineType.BOTH, 12, 14),
+ new GrDiffLine(GrDiffLineType.BOTH, 13, 15),
+ new GrDiffLine(GrDiffLineType.BOTH, 14, 16),
+ ],
+ }),
];
});
@@ -140,21 +155,25 @@
assert.equal(collapsedGroups[2].contextGroups.length, 2);
assert.equal(
- collapsedGroups[2].contextGroups[0].type,
- GrDiffGroupType.DELTA);
+ collapsedGroups[2].contextGroups[0].type,
+ GrDiffGroupType.DELTA
+ );
assert.deepEqual(
- collapsedGroups[2].contextGroups[0].adds,
- groups[1].adds.slice(1));
+ collapsedGroups[2].contextGroups[0].adds,
+ groups[1].adds.slice(1)
+ );
assert.deepEqual(
- collapsedGroups[2].contextGroups[0].removes,
- groups[1].removes.slice(1));
+ collapsedGroups[2].contextGroups[0].removes,
+ groups[1].removes.slice(1)
+ );
assert.equal(
- collapsedGroups[2].contextGroups[1].type,
- GrDiffGroupType.BOTH);
- assert.deepEqual(
- collapsedGroups[2].contextGroups[1].lines,
- [groups[2].lines[0]]);
+ collapsedGroups[2].contextGroups[1].type,
+ GrDiffGroupType.BOTH
+ );
+ assert.deepEqual(collapsedGroups[2].contextGroups[1].lines, [
+ groups[2].lines[0],
+ ]);
assert.equal(collapsedGroups[3].type, GrDiffGroupType.BOTH);
assert.deepEqual(collapsedGroups[3].lines, groups[2].lines.slice(1));
@@ -166,19 +185,26 @@
type: GrDiffGroupType.BOTH,
skip: 60,
offsetLeft: 8,
- offsetRight: 10});
+ offsetRight: 10,
+ });
groups = [
- 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({
+ 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({type: GrDiffGroupType.BOTH, lines: [
- new GrDiffLine(GrDiffLineType.BOTH, 68, 70),
- new GrDiffLine(GrDiffLineType.BOTH, 69, 71),
- new GrDiffLine(GrDiffLineType.BOTH, 70, 72),
- ]}),
+ new GrDiffGroup({
+ type: GrDiffGroupType.BOTH,
+ lines: [
+ new GrDiffLine(GrDiffLineType.BOTH, 68, 70),
+ new GrDiffLine(GrDiffLineType.BOTH, 69, 71),
+ new GrDiffLine(GrDiffLineType.BOTH, 70, 72),
+ ],
+ }),
];
});
@@ -189,13 +215,11 @@
});
test('groups unchanged if the hidden range is empty', () => {
- assert.deepEqual(
- hideInContextControl(groups, 0, 0), groups);
+ assert.deepEqual(hideInContextControl(groups, 0, 0), groups);
});
test('groups unchanged if there is only 1 line to hide', () => {
- assert.deepEqual(
- hideInContextControl(groups, 3, 4), groups);
+ assert.deepEqual(hideInContextControl(groups, 3, 4), groups);
});
});
@@ -206,7 +230,7 @@
lines.push(new GrDiffLine(GrDiffLineType.ADD));
}
const group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
- assert.isTrue(group.isTotal(group));
+ assert.isTrue(group.isTotal());
});
test('is total for remove', () => {
@@ -215,12 +239,7 @@
lines.push(new GrDiffLine(GrDiffLineType.REMOVE));
}
const group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
- assert.isTrue(group.isTotal(group));
- });
-
- test('not total for empty', () => {
- const group = new GrDiffGroup({type: GrDiffGroupType.BOTH});
- assert.isFalse(group.isTotal(group));
+ assert.isTrue(group.isTotal());
});
test('not total for non-delta', () => {
@@ -229,8 +248,7 @@
lines.push(new GrDiffLine(GrDiffLineType.BOTH));
}
const group = new GrDiffGroup({type: GrDiffGroupType.DELTA, lines});
- assert.isFalse(group.isTotal(group));
+ assert.isFalse(group.isTotal());
});
});
});
-
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
index e5f9de0..27952d3 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -43,11 +43,7 @@
import {getHiddenScroll} from '../../../scripts/hiddenscroll';
import {customElement, observe, property} from '@polymer/decorators';
import {BlameInfo, CommentRange, ImageInfo} from '../../../types/common';
-import {
- DiffInfo,
- DiffPreferencesInfo,
- DiffPreferencesInfoKey,
-} from '../../../types/diff';
+import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
import {GrDiffHighlight} from '../gr-diff-highlight/gr-diff-highlight';
import {
GrDiffBuilderElement,
@@ -81,6 +77,7 @@
import {assertIsDefined} from '../../../utils/common-util';
import {debounce, DelayedTask} from '../../../utils/async-util';
import {GrDiffSelection} from '../gr-diff-selection/gr-diff-selection';
+import {deepEqual} from '../../../utils/deep-util';
const NO_NEWLINE_LEFT = 'No newline at end of left file.';
const NO_NEWLINE_RIGHT = 'No newline at end of right file.';
@@ -100,7 +97,6 @@
export interface GrDiff {
$: {
- diffBuilder: GrDiffBuilderElement;
diffTable: HTMLTableElement;
};
}
@@ -179,7 +175,7 @@
@property({type: Object})
highlightRange?: CommentRange;
- @property({type: Array})
+ @property({type: Array, observer: '_coverageRangesObserver'})
coverageRanges: CoverageRange[] = [];
@property({type: Boolean, observer: '_lineWrappingObserver'})
@@ -248,9 +244,6 @@
@property({type: Object, observer: '_blameChanged'})
blame: BlameInfo[] | null = null;
- @property({type: Number})
- parentIndex?: number;
-
@property({type: Boolean})
showNewlineWarningLeft = false;
@@ -292,11 +285,16 @@
@property({type: Boolean})
isAttached = false;
- private renderDiffTableTask?: DelayedTask;
+ // visible for testing
+ renderDiffTableTask?: DelayedTask;
private diffSelection = new GrDiffSelection();
- private highlights = new GrDiffHighlight();
+ // visible for testing
+ highlights = new GrDiffHighlight();
+
+ // visible for testing
+ diffBuilder = new GrDiffBuilderElement();
constructor() {
super();
@@ -321,11 +319,12 @@
this._unobserveNodes();
this.diffSelection.cleanup();
this.highlights.cleanup();
+ this.diffBuilder.cancel();
super.disconnectedCallback();
}
getLineNumEls(side: Side): HTMLElement[] {
- return this.$.diffBuilder.getLineNumEls(side);
+ return this.diffBuilder.getLineNumEls(side);
}
showNoChangeMessage(
@@ -426,19 +425,25 @@
cr.side === removedCommentRange.side &&
rangesEqual(cr.range, removedCommentRange.range)
);
- this.splice('_commentRanges', i, 1);
+ this._commentRanges.splice(i, 1);
}
- if (addedCommentRanges && addedCommentRanges.length) {
- this.push('_commentRanges', ...addedCommentRanges);
+ if (addedCommentRanges?.length) {
+ this._commentRanges.push(...addedCommentRanges);
}
if (this.highlightRange) {
- this.push('_commentRanges', {
+ this._commentRanges.push({
side: Side.RIGHT,
range: this.highlightRange,
rootId: '',
});
}
+
+ this.diffBuilder.updateCommentRanges(this._commentRanges);
+ }
+
+ _coverageRangesObserver() {
+ this.diffBuilder.updateCoverageRanges(this.coverageRanges);
}
/**
@@ -483,7 +488,7 @@
/** Cancel any remaining diff builder rendering work. */
cancel() {
- this.$.diffBuilder.cancel();
+ this.diffBuilder.cancel();
this.renderDiffTableTask?.cancel();
}
@@ -492,7 +497,7 @@
// Get rendered stops.
const stops: Array<HTMLElement | AbortStop> =
- this.$.diffBuilder.getLineNumberRows();
+ this.diffBuilder.getLineNumberRows();
// If we are still loading this diff, abort after the rendered stops to
// avoid skipping over to e.g. the next file.
@@ -512,7 +517,7 @@
_blameChanged(newValue?: BlameInfo[] | null) {
if (newValue === undefined) return;
- this.$.diffBuilder.setBlame(newValue);
+ this.diffBuilder.setBlame(newValue);
if (newValue) {
this.classList.add('showBlame');
} else {
@@ -534,7 +539,7 @@
return classes.join(' ');
}
- _handleTap(e: CustomEvent) {
+ _handleTap(e: Event) {
const el = (dom(e) as EventApi).localTarget as Element;
if (
@@ -603,7 +608,7 @@
_createCommentForSelection(side: Side, range: CommentRange) {
const lineNum = range.end_line;
- const lineEl = this.$.diffBuilder.getLineElByNumber(lineNum, side);
+ const lineEl = this.diffBuilder.getLineElByNumber(lineNum, side);
if (lineEl) {
this._createComment(lineEl, lineNum, side, range);
}
@@ -621,7 +626,7 @@
side?: Side,
range?: CommentRange
) {
- const contentEl = this.$.diffBuilder.getContentTdByLineEl(lineEl);
+ const contentEl = this.diffBuilder.getContentTdByLineEl(lineEl);
if (!contentEl) throw new Error('content el not found for line el');
side = side ?? this._getCommentSideByLineAndContent(lineEl, contentEl);
assertIsDefined(this.path, 'path');
@@ -663,28 +668,11 @@
}
_prefsObserver(newPrefs: DiffPreferencesInfo, oldPrefs: DiffPreferencesInfo) {
- if (!this._prefsEqual(newPrefs, oldPrefs)) {
+ if (!deepEqual(newPrefs, oldPrefs)) {
this._prefsChanged(newPrefs);
}
}
- _prefsEqual(prefs1: DiffPreferencesInfo, prefs2: DiffPreferencesInfo) {
- if (prefs1 === prefs2) {
- return true;
- }
- if (!prefs1 || !prefs2) {
- return false;
- }
- // Scan the preference objects one level deep to see if they differ.
- const keys1 = Object.keys(prefs1) as DiffPreferencesInfoKey[];
- const keys2 = Object.keys(prefs2) as DiffPreferencesInfoKey[];
- return (
- keys1.length === keys2.length &&
- keys1.every(key => prefs1[key] === prefs2[key]) &&
- keys2.every(key => prefs1[key] === prefs2[key])
- );
- }
-
_pathObserver() {
// Call _prefsChanged(), because line-limit style value depends on path.
this._prefsChanged(this.prefs);
@@ -699,7 +687,7 @@
if (!this.lineOfInterest) return;
const lineNum = this.lineOfInterest.lineNum;
if (typeof lineNum !== 'number') return;
- this.$.diffBuilder.unhideLine(lineNum, this.lineOfInterest.side);
+ this.diffBuilder.unhideLine(lineNum, this.lineOfInterest.side);
}
_cleanup() {
@@ -808,7 +796,7 @@
if (this.prefs) {
this._updatePreferenceStyles(this.prefs, renderPrefs);
}
- this.$.diffBuilder.updateRenderPrefs(renderPrefs);
+ this.diffBuilder.updateRenderPrefs(renderPrefs);
}
_diffChanged(newValue?: DiffInfo) {
@@ -820,7 +808,7 @@
}
if (this.diff) {
this.diffSelection.init(this.diff, this.$.diffTable);
- this.highlights.init(this.$.diffTable, this.$.diffBuilder);
+ this.highlights.init(this.$.diffTable, this.diffBuilder);
}
}
@@ -866,9 +854,24 @@
this._showWarning = false;
const keyLocations = this._computeKeyLocations();
- this.$.diffBuilder.prefs = this._getBypassPrefs(this.prefs);
- this.$.diffBuilder.renderPrefs = this.renderPrefs;
- this.$.diffBuilder.render(keyLocations);
+
+ // TODO: Setting tons of public properties like this is obviously a code
+ // smell. We are planning to introduce a diff model for managing all this
+ // data. Then diff builder will only need access to that model.
+ this.diffBuilder.prefs = this._getBypassPrefs(this.prefs);
+ this.diffBuilder.renderPrefs = this.renderPrefs;
+ this.diffBuilder.diff = this.diff;
+ this.diffBuilder.path = this.path;
+ this.diffBuilder.viewMode = this.viewMode;
+ this.diffBuilder.layers = this.layers ?? [];
+ this.diffBuilder.isImageDiff = this.isImageDiff;
+ this.diffBuilder.baseImage = this.baseImage ?? null;
+ this.diffBuilder.revisionImage = this.revisionImage ?? null;
+ this.diffBuilder.useNewImageDiffUi = this.useNewImageDiffUi;
+ this.diffBuilder.diffElement = this.$.diffTable;
+ this.diffBuilder.updateCommentRanges(this._commentRanges);
+ this.diffBuilder.updateCoverageRanges(this.coverageRanges);
+ this.diffBuilder.render(keyLocations);
}
_handleRenderContent() {
@@ -895,10 +898,7 @@
const commentSide = getSide(threadEl);
const range = getRange(threadEl);
if (!commentSide) continue;
- const lineEl = this.$.diffBuilder.getLineElByNumber(
- lineNum,
- commentSide
- );
+ const lineEl = this.diffBuilder.getLineElByNumber(lineNum, commentSide);
// When the line the comment refers to does not exist, log an error
// but don't crash. This can happen e.g. if the API does not fully
// validate e.g. (robot) comments
@@ -911,7 +911,7 @@
);
continue;
}
- const contentEl = this.$.diffBuilder.getContentTdByLineEl(lineEl);
+ const contentEl = this.diffBuilder.getContentTdByLineEl(lineEl);
if (!contentEl) continue;
if (lineNum === 'LOST' && !contentEl.hasChildNodes()) {
contentEl.appendChild(this._portedCommentsWithoutRangeMessage());
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts
index 6d36b89..a1082f9 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_html.ts
@@ -349,7 +349,7 @@
background-color: var(--dark-rebased-remove-highlight-color);
}
.dueToRebase .content.remove .contentText {
- background-color: var(--light-remove-add-highlight-color);
+ background-color: var(--light-rebased-remove-highlight-color);
}
/* dueToMove */
@@ -698,36 +698,22 @@
class$="[[_computeContainerClass(loggedIn, viewMode, displayLine)]]"
on-click="_handleTap"
>
- <gr-diff-builder
- id="diffBuilder"
- comment-ranges="[[_commentRanges]]"
- coverage-ranges="[[coverageRanges]]"
- diff="[[diff]]"
- path="[[path]]"
- view-mode="[[viewMode]]"
- is-image-diff="[[isImageDiff]]"
- base-image="[[baseImage]]"
- layers="[[layers]]"
- revision-image="[[revisionImage]]"
- use-new-image-diff-ui="[[useNewImageDiffUi]]"
- >
- <table
- id="diffTable"
- class$="[[_diffTableClass]]"
- role="presentation"
- contenteditable$="[[isContentEditable]]"
- ></table>
+ <table
+ id="diffTable"
+ class$="[[_diffTableClass]]"
+ role="presentation"
+ contenteditable$="[[isContentEditable]]"
+ ></table>
- <template
- is="dom-if"
- if="[[showNoChangeMessage(_loading, prefs, _diffLength, diff)]]"
- >
- <div class="whitespace-change-only-message">
- This file only contains whitespace changes. Modify the whitespace
- setting to see the changes.
- </div>
- </template>
- </gr-diff-builder>
+ <template
+ is="dom-if"
+ if="[[showNoChangeMessage(_loading, prefs, _diffLength, diff)]]"
+ >
+ <div class="whitespace-change-only-message">
+ This file only contains whitespace changes. Modify the whitespace
+ setting to see the changes.
+ </div>
+ </template>
</div>
<div class$="[[_computeNewlineWarningClass(_newlineWarning, _loading)]]">
[[_newlineWarning]]
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.js b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
similarity index 62%
rename from polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.js
rename to polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
index c8b643d..1422223 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.js
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
@@ -1,32 +1,37 @@
/**
* @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.
+ * Copyright 2015 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
*/
-import '../../../test/common-test-setup-karma.js';
-import {createDiff} from '../../../test/test-data-generators.js';
-import './gr-diff.js';
-import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image.js';
-import {getComputedStyleValue} from '../../../utils/dom-util.js';
-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';
-import {AbortStop} from '../../../api/core.js';
-import {afterNextRender} from '@polymer/polymer/lib/utils/render-status';
-import {waitForEventOnce} from '../../../utils/event-util.js';
+import '../../../test/common-test-setup-karma';
+import {createDiff} from '../../../test/test-data-generators';
+import './gr-diff';
+import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image';
+import {getComputedStyleValue} from '../../../utils/dom-util';
+import {_setHiddenScroll} from '../../../scripts/hiddenscroll';
+import {runA11yAudit} from '../../../test/a11y-test-utils';
+import '@polymer/paper-button/paper-button';
+import {
+ DiffContent,
+ DiffInfo,
+ DiffPreferencesInfo,
+ DiffViewMode,
+ IgnoreWhitespaceType,
+ Side,
+} from '../../../api/diff';
+import {
+ mockPromise,
+ mouseDown,
+ query,
+ queryAll,
+ queryAndAssert,
+ stubRestApi,
+} from '../../../test/test-utils';
+import {AbortStop} from '../../../api/core';
+import {waitForEventOnce} from '../../../utils/event-util';
+import {GrDiff} from './gr-diff';
+import {ImageInfo} from '../../../types/common';
+import {GrRangedCommentHint} from '../gr-ranged-comment-hint/gr-ranged-comment-hint';
const basicFixture = fixtureFromElement('gr-diff');
@@ -37,42 +42,51 @@
});
suite('gr-diff tests', () => {
- let element;
+ let element: GrDiff;
- const MINIMAL_PREFS = {tab_size: 2, line_length: 80, font_size: 12};
+ const MINIMAL_PREFS: DiffPreferencesInfo = {
+ tab_size: 2,
+ line_length: 80,
+ font_size: 12,
+ context: 3,
+ ignore_whitespace: 'IGNORE_NONE',
+ };
- setup(() => {
-
- });
+ setup(() => {});
suite('selectionchange event handling', () => {
- const emulateSelection = function() {
+ let handleSelectionChangeStub: sinon.SinonSpy;
+
+ const emulateSelection = function () {
document.dispatchEvent(new CustomEvent('selectionchange'));
};
setup(() => {
element = basicFixture.instantiate();
- sinon.stub(element.highlights, 'handleSelectionChange');
+ handleSelectionChangeStub = sinon.spy(
+ element.highlights,
+ 'handleSelectionChange'
+ );
});
test('enabled if logged in', async () => {
element.loggedIn = true;
emulateSelection();
await flush();
- assert.isTrue(element.highlights.handleSelectionChange.called);
+ assert.isTrue(handleSelectionChangeStub.called);
});
test('ignored if logged out', async () => {
element.loggedIn = false;
emulateSelection();
await flush();
- assert.isFalse(element.highlights.handleSelectionChange.called);
+ assert.isFalse(handleSelectionChangeStub.called);
});
});
test('cancel', () => {
element = basicFixture.instantiate();
- const cancelStub = sinon.stub(element.$.diffBuilder, 'cancel');
+ const cancelStub = sinon.stub(element.diffBuilder, 'cancel');
element.cancel();
assert.isTrue(cancelStub.calledOnce);
});
@@ -98,10 +112,12 @@
});
test('line limit is based on line_length', () => {
- element.prefs = {...element.prefs, line_length: 100};
+ element.prefs = {...element.prefs!, line_length: 100};
flush();
- assert.equal(getComputedStyleValue('--line-limit-marker', element),
- '100ch');
+ assert.equal(
+ getComputedStyleValue('--line-limit-marker', element),
+ '100ch'
+ );
});
test('content-width should not be defined', () => {
@@ -123,32 +139,40 @@
});
test('max-width considers two content columns in side-by-side', () => {
- element.viewMode = 'SIDE_BY_SIDE';
+ element.viewMode = DiffViewMode.SIDE_BY_SIDE;
flush();
- assert.equal(getComputedStyleValue('--diff-max-width', element),
- 'calc(2 * 80ch + 2 * 48px + 0ch + 1px + 2px)');
+ assert.equal(
+ getComputedStyleValue('--diff-max-width', element),
+ 'calc(2 * 80ch + 2 * 48px + 0ch + 1px + 2px)'
+ );
});
test('max-width considers one content column in unified', () => {
- element.viewMode = 'UNIFIED_DIFF';
+ element.viewMode = DiffViewMode.UNIFIED;
flush();
- assert.equal(getComputedStyleValue('--diff-max-width', element),
- 'calc(1 * 80ch + 2 * 48px + 0ch + 1px + 2px)');
+ assert.equal(
+ getComputedStyleValue('--diff-max-width', element),
+ 'calc(1 * 80ch + 2 * 48px + 0ch + 1px + 2px)'
+ );
});
test('max-width considers font-size', () => {
- element.prefs = {...element.prefs, font_size: 13};
+ element.prefs = {...element.prefs!, font_size: 13};
flush();
// Each line number column: 4 * 13 = 52px
- assert.equal(getComputedStyleValue('--diff-max-width', element),
- 'calc(2 * 80ch + 2 * 52px + 0ch + 1px + 2px)');
+ assert.equal(
+ getComputedStyleValue('--diff-max-width', element),
+ 'calc(2 * 80ch + 2 * 52px + 0ch + 1px + 2px)'
+ );
});
test('sign cols are considered if show_sign_col is true', () => {
element.renderPrefs = {...element.renderPrefs, show_sign_col: true};
flush();
- assert.equal(getComputedStyleValue('--diff-max-width', element),
- 'calc(2 * 80ch + 2 * 48px + 2ch + 1px + 2px)');
+ assert.equal(
+ getComputedStyleValue('--diff-max-width', element),
+ 'calc(2 * 80ch + 2 * 48px + 2ch + 1px + 2px)'
+ );
});
});
@@ -168,39 +192,31 @@
});
test('view does not start with displayLine classList', () => {
- assert.isFalse(
- element.shadowRoot
- .querySelector('.diffContainer')
- .classList
- .contains('displayLine'));
+ const container = queryAndAssert(element, '.diffContainer');
+ assert.isFalse(container.classList.contains('displayLine'));
});
test('displayLine class added called when displayLine is true', () => {
const spy = sinon.spy(element, '_computeContainerClass');
element.displayLine = true;
+ const container = queryAndAssert(element, '.diffContainer');
assert.isTrue(spy.called);
- assert.isTrue(
- element.shadowRoot
- .querySelector('.diffContainer')
- .classList
- .contains('displayLine'));
+ assert.isTrue(container.classList.contains('displayLine'));
});
test('thread groups', () => {
const contentEl = document.createElement('div');
- element.changeNum = 123;
- element.patchRange = {basePatchNum: 1, patchNum: 2};
element.path = 'file.txt';
- element.$.diffBuilder.diff = createDiff();
- element.$.diffBuilder.prefs = {...MINIMAL_PREFS};
- element.$.diffBuilder._builder = element.$.diffBuilder._getDiffBuilder();
// No thread groups.
assert.equal(contentEl.querySelectorAll('.thread-group').length, 0);
// A thread group gets created.
- const threadGroupEl = element._getOrCreateThreadGroup(contentEl);
+ const threadGroupEl = element._getOrCreateThreadGroup(
+ contentEl,
+ Side.LEFT
+ );
assert.isOk(threadGroupEl);
// The new thread group can be fetched.
@@ -208,17 +224,19 @@
});
suite('image diffs', () => {
- let mockFile1;
- let mockFile2;
+ let mockFile1: ImageInfo;
+ let mockFile2: ImageInfo;
setup(() => {
mockFile1 = {
- body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
- 'wsAAAAAAAAAAAAAAAAA/w==',
+ body:
+ 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
+ 'wsAAAAAAAAAAAAAAAAA/w==',
type: 'image/bmp',
};
mockFile2 = {
- body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
- 'wsAAAAAAAAAAAAA/////w==',
+ body:
+ 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
+ 'wsAAAAAAAAAAAAA/////w==',
type: 'image/bmp',
};
@@ -235,7 +253,6 @@
show_whitespace_errors: true,
syntax_highlighting: true,
tab_size: 8,
- theme: 'DEFAULT',
};
});
@@ -244,8 +261,7 @@
element.revisionImage = mockFile2;
element.diff = {
meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
intraline_status: 'OK',
change_type: 'MODIFIED',
diff_header: [
@@ -262,42 +278,40 @@
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
+ assert.instanceOf(element.diffBuilder.builder, GrDiffBuilderImage);
// Left image rendered with the parent commit's version of the file.
- const leftImage = element.$.diffTable.querySelector('td.left img');
- const leftLabel =
- element.$.diffTable.querySelector('td.left label');
- const leftLabelContent = leftLabel.querySelector('.label');
- const leftLabelName = leftLabel.querySelector('.name');
+ const diffTable = element.$.diffTable;
+ const leftImage = queryAndAssert(diffTable, 'td.left img');
+ const leftLabel = queryAndAssert(diffTable, 'td.left label');
+ const leftLabelContent = queryAndAssert(leftLabel, '.label');
+ const leftLabelName = query(leftLabel, '.name');
- const rightImage =
- element.$.diffTable.querySelector('td.right img');
- const rightLabel = element.$.diffTable.querySelector(
- 'td.right label');
- const rightLabelContent = rightLabel.querySelector('.label');
- const rightLabelName = rightLabel.querySelector('.name');
+ const rightImage = queryAndAssert(diffTable, 'td.right img');
+ const rightLabel = queryAndAssert(diffTable, 'td.right label');
+ const rightLabelContent = queryAndAssert(rightLabel, '.label');
+ const rightLabelName = query(rightLabel, '.name');
assert.isNotOk(rightLabelName);
assert.isNotOk(leftLabelName);
- assert.isOk(leftImage);
- assert.equal(leftImage.getAttribute('src'),
- 'data:image/bmp;base64,' + mockFile1.body);
- assert.equal(leftLabelContent.textContent, '1\u00d71 image/bmp');// \u00d7 - '×'
+ assert.equal(
+ leftImage.getAttribute('src'),
+ 'data:image/bmp;base64,' + mockFile1.body
+ );
+ assert.isTrue(leftLabelContent.textContent?.includes('image/bmp'));
- assert.isOk(rightImage);
- assert.equal(rightImage.getAttribute('src'),
- 'data:image/bmp;base64,' + mockFile2.body);
- assert.equal(rightLabelContent.textContent, '1\u00d71 image/bmp');// \u00d7 - '×'
+ assert.equal(
+ rightImage.getAttribute('src'),
+ 'data:image/bmp;base64,' + mockFile2.body
+ );
+ assert.isTrue(rightLabelContent.textContent?.includes('image/bmp'));
});
test('renders image diffs with a different file name', async () => {
- const mockDiff = {
+ const mockDiff: DiffInfo = {
meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg',
- lines: 560},
+ meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg', lines: 560},
intraline_status: 'OK',
change_type: 'MODIFIED',
diff_header: [
@@ -312,51 +326,51 @@
};
element.baseImage = mockFile1;
- element.baseImage._name = mockDiff.meta_a.name;
+ element.baseImage._name = mockDiff.meta_a!.name;
element.revisionImage = mockFile2;
- element.revisionImage._name = mockDiff.meta_b.name;
+ element.revisionImage._name = mockDiff.meta_b!.name;
element.diff = mockDiff;
await waitForEventOnce(element, 'render');
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
+ assert.instanceOf(element.diffBuilder.builder, GrDiffBuilderImage);
// Left image rendered with the parent commit's version of the file.
- const leftImage = element.$.diffTable.querySelector('td.left img');
- const leftLabel =
- element.$.diffTable.querySelector('td.left label');
- const leftLabelContent = leftLabel.querySelector('.label');
- const leftLabelName = leftLabel.querySelector('.name');
+ const diffTable = element.$.diffTable;
+ const leftImage = queryAndAssert(diffTable, 'td.left img');
+ const leftLabel = queryAndAssert(diffTable, 'td.left label');
+ const leftLabelContent = queryAndAssert(leftLabel, '.label');
+ const leftLabelName = queryAndAssert(leftLabel, '.name');
- const rightImage =
- element.$.diffTable.querySelector('td.right img');
- const rightLabel = element.$.diffTable.querySelector(
- 'td.right label');
- const rightLabelContent = rightLabel.querySelector('.label');
- const rightLabelName = rightLabel.querySelector('.name');
+ const rightImage = queryAndAssert(diffTable, 'td.right img');
+ const rightLabel = queryAndAssert(diffTable, 'td.right label');
+ const rightLabelContent = queryAndAssert(rightLabel, '.label');
+ const rightLabelName = queryAndAssert(rightLabel, '.name');
assert.isOk(rightLabelName);
assert.isOk(leftLabelName);
- assert.equal(leftLabelName.textContent, mockDiff.meta_a.name);
- assert.equal(rightLabelName.textContent, mockDiff.meta_b.name);
+ assert.equal(leftLabelName.textContent, mockDiff.meta_a?.name);
+ assert.equal(rightLabelName.textContent, mockDiff.meta_b?.name);
assert.isOk(leftImage);
- assert.equal(leftImage.getAttribute('src'),
- 'data:image/bmp;base64,' + mockFile1.body);
- assert.equal(leftLabelContent.textContent, '1\u00d71 image/bmp');// \u00d7 - '×'
+ assert.equal(
+ leftImage.getAttribute('src'),
+ 'data:image/bmp;base64,' + mockFile1.body
+ );
+ assert.isTrue(leftLabelContent.textContent?.includes('image/bmp'));
assert.isOk(rightImage);
- assert.equal(rightImage.getAttribute('src'),
- 'data:image/bmp;base64,' + mockFile2.body);
- assert.equal(rightLabelContent.textContent, '1\u00d71 image/bmp');// \u00d7 - '×'
+ assert.equal(
+ rightImage.getAttribute('src'),
+ 'data:image/bmp;base64,' + mockFile2.body
+ );
+ assert.isTrue(rightLabelContent.textContent?.includes('image/bmp'));
});
test('renders added image', async () => {
- const mockDiff = {
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
+ const mockDiff: DiffInfo = {
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
intraline_status: 'OK',
change_type: 'ADDED',
diff_header: [
@@ -371,7 +385,9 @@
};
const promise = mockPromise();
- function rendered() { promise.resolve(); }
+ function rendered() {
+ promise.resolve();
+ }
element.addEventListener('render', rendered);
element.revisionImage = mockFile2;
@@ -380,20 +396,17 @@
element.removeEventListener('render', rendered);
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
+ assert.instanceOf(element.diffBuilder.builder, GrDiffBuilderImage);
- const leftImage = element.$.diffTable.querySelector('td.left img');
- const rightImage = element.$.diffTable.querySelector('td.right img');
-
+ const diffTable = element.$.diffTable;
+ const leftImage = query(diffTable, 'td.left img');
assert.isNotOk(leftImage);
- assert.isOk(rightImage);
+ queryAndAssert(diffTable, 'td.right img');
});
test('renders removed image', async () => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
+ const mockDiff: DiffInfo = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
intraline_status: 'OK',
change_type: 'DELETED',
diff_header: [
@@ -407,7 +420,9 @@
binary: true,
};
const promise = mockPromise();
- function rendered() { promise.resolve(); }
+ function rendered() {
+ promise.resolve();
+ }
element.addEventListener('render', rendered);
element.baseImage = mockFile1;
@@ -416,20 +431,21 @@
element.removeEventListener('render', rendered);
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
+ assert.instanceOf(element.diffBuilder.builder, GrDiffBuilderImage);
- const leftImage = element.$.diffTable.querySelector('td.left img');
- const rightImage = element.$.diffTable.querySelector('td.right img');
-
- assert.isOk(leftImage);
+ const diffTable = element.$.diffTable;
+ queryAndAssert(diffTable, 'td.left img');
+ const rightImage = query(diffTable, 'td.right img');
assert.isNotOk(rightImage);
});
test('does not render disallowed image type', async () => {
- const mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil',
- lines: 560},
+ const mockDiff: DiffInfo = {
+ meta_a: {
+ name: 'carrot.jpg',
+ content_type: 'image/jpeg-evil',
+ lines: 560,
+ },
intraline_status: 'OK',
change_type: 'DELETED',
diff_header: [
@@ -445,7 +461,9 @@
mockFile1.type = 'image/jpeg-evil';
const promise = mockPromise();
- function rendered() { promise.resolve(); }
+ function rendered() {
+ promise.resolve();
+ }
element.addEventListener('render', rendered);
element.baseImage = mockFile1;
@@ -454,9 +472,9 @@
element.removeEventListener('render', rendered);
// Recognizes that it should be an image diff.
assert.isTrue(element.isImageDiff);
- assert.instanceOf(
- element.$.diffBuilder._builder, GrDiffBuilderImage);
- const leftImage = element.$.diffTable.querySelector('td.left img');
+ assert.instanceOf(element.diffBuilder.builder, GrDiffBuilderImage);
+ const diffTable = element.$.diffTable;
+ const leftImage = query(diffTable, 'td.left img');
assert.isNotOk(leftImage);
});
});
@@ -513,7 +531,6 @@
show_tabs: true,
show_whitespace_errors: true,
syntax_highlighting: true,
- theme: 'DEFAULT',
ignore_whitespace: 'IGNORE_NONE',
};
@@ -548,20 +565,20 @@
const FILE_ROW = 1;
const actual = element.getCursorStops();
assert.equal(actual.length, ROWS + FILE_ROW + 1);
- assert.isTrue(actual[actual.length -1] instanceof AbortStop);
+ assert.isTrue(actual[actual.length - 1] instanceof AbortStop);
});
});
test('adds .hiddenscroll', () => {
_setHiddenScroll(true);
element.displayLine = true;
- assert.include(element.shadowRoot
- .querySelector('.diffContainer').className, 'hiddenscroll');
+ const container = queryAndAssert(element, '.diffContainer');
+ assert.include(container.className, 'hiddenscroll');
});
});
suite('logged in', () => {
- let fakeLineEl;
+ let fakeLineEl: HTMLElement;
setup(() => {
element = basicFixture.instantiate();
element.loggedIn = true;
@@ -571,15 +588,14 @@
classList: {
contains: sinon.stub().returns(true),
},
- };
+ } as unknown as HTMLElement;
});
test('addDraftAtLine', () => {
sinon.stub(element, '_selectLine');
- sinon.stub(element, '_createComment');
+ const createCommentStub = sinon.stub(element, '_createComment');
element.addDraftAtLine(fakeLineEl);
- assert.isTrue(element._createComment
- .calledWithExactly(fakeLineEl, 42));
+ assert.isTrue(createCommentStub.calledWithExactly(fakeLineEl, 42));
});
test('adds long range comment hint', async () => {
@@ -592,23 +608,28 @@
const threadEl = document.createElement('div');
threadEl.className = 'comment-thread';
threadEl.setAttribute('diff-side', 'right');
- threadEl.setAttribute('line-num', 1);
+ threadEl.setAttribute('line-num', '1');
threadEl.setAttribute('range', JSON.stringify(range));
threadEl.setAttribute('slot', 'right-1');
- const content = [{
- a: [],
- b: [],
- }, {
- ab: Array(13).fill('text'),
- }];
+ const content = [
+ {
+ a: ['asdf'],
+ },
+ {
+ ab: Array(13).fill('text'),
+ },
+ ];
setupSampleDiff({content});
- await new Promise(resolve => afterNextRender(element, resolve));
+ await waitForEventOnce(element, 'render');
element.appendChild(threadEl);
- await flush();
+ await waitForEventOnce(element, 'render');
- assert.deepEqual(
- element.querySelector('gr-ranged-comment-hint').range, range);
+ const hint = queryAndAssert<GrRangedCommentHint>(
+ element,
+ 'gr-ranged-comment-hint'
+ );
+ assert.deepEqual(hint.range, range);
});
test('no duplicate range hint for same thread', async () => {
@@ -621,109 +642,119 @@
const threadEl = document.createElement('div');
threadEl.className = 'comment-thread';
threadEl.setAttribute('diff-side', 'right');
- threadEl.setAttribute('line-num', 1);
+ threadEl.setAttribute('line-num', '1');
threadEl.setAttribute('range', JSON.stringify(range));
threadEl.setAttribute('slot', 'right-1');
const firstHint = document.createElement('gr-ranged-comment-hint');
firstHint.range = range;
- firstHint.setAttribute('threadElRootId', threadEl.rootId);
firstHint.setAttribute('slot', 'right-1');
- const content = [{
- a: [],
- b: [],
- }, {
- ab: Array(13).fill('text'),
- }];
+ const content = [
+ {
+ a: ['asdf'],
+ },
+ {
+ ab: Array(13).fill('text'),
+ },
+ ];
setupSampleDiff({content});
+ await waitForEventOnce(element, 'render');
element.appendChild(firstHint);
- await flush();
- element._handleRenderContent();
- await flush();
+ element.appendChild(threadEl);
+ await waitForEventOnce(element, 'render');
+
+ assert.equal(
+ element.querySelectorAll('gr-ranged-comment-hint').length,
+ 1
+ );
+ });
+
+ test('removes long range comment hint when comment is discarded', async () => {
+ const range = {
+ start_line: 1,
+ end_line: 7,
+ start_character: 0,
+ end_character: 0,
+ };
+ const threadEl = document.createElement('div');
+ threadEl.className = 'comment-thread';
+ threadEl.setAttribute('diff-side', 'right');
+ threadEl.setAttribute('line-num', '1');
+ threadEl.setAttribute('range', JSON.stringify(range));
+ threadEl.setAttribute('slot', 'right-1');
+ const content = [
+ {
+ a: [],
+ b: [],
+ },
+ {
+ ab: Array(8).fill('text'),
+ },
+ ];
+ setupSampleDiff({content});
element.appendChild(threadEl);
await flush();
- assert.equal(
- element.querySelectorAll('gr-ranged-comment-hint').length, 1);
+ threadEl.remove();
+ await flush();
+
+ assert.isEmpty(element.querySelectorAll('gr-ranged-comment-hint'));
});
- test('removes long range comment hint when comment is discarded',
- async () => {
- const range = {
- start_line: 1,
- end_line: 7,
- start_character: 0,
- end_character: 0,
- };
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('diff-side', 'right');
- threadEl.setAttribute('line-num', 1);
- threadEl.setAttribute('range', JSON.stringify(range));
- threadEl.setAttribute('slot', 'right-1');
- const content = [{
- a: [],
- b: [],
- }, {
- ab: Array(8).fill('text'),
- }];
- setupSampleDiff({content});
- element.appendChild(threadEl);
- await flush();
-
- threadEl.remove();
- await flush();
-
- assert.isEmpty(element.querySelectorAll('gr-ranged-comment-hint'));
- });
-
suite('change in preferences', () => {
setup(() => {
element.diff = {
meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
diff_header: [],
intraline_status: 'OK',
change_type: 'MODIFIED',
content: [{skip: 66}],
};
- element.renderDiffTableTask.flush();
+ element.renderDiffTableTask?.flush();
});
test('change in preferences re-renders diff', () => {
- sinon.stub(element, '_renderDiffTable');
+ const stub = sinon.stub(element, '_renderDiffTable');
element.prefs = {
- ...MINIMAL_PREFS, time_format: 'HHMM_12'};
- element.renderDiffTableTask.flush();
- assert.isTrue(element._renderDiffTable.called);
+ ...MINIMAL_PREFS,
+ };
+ element.renderDiffTableTask?.flush();
+ assert.isTrue(stub.called);
});
test('adding/removing property in preferences re-renders diff', () => {
const stub = sinon.stub(element, '_renderDiffTable');
- const newPrefs1 = {...MINIMAL_PREFS,
- line_wrapping: true};
+ const newPrefs1: DiffPreferencesInfo = {
+ ...MINIMAL_PREFS,
+ line_wrapping: true,
+ };
element.prefs = newPrefs1;
- element.renderDiffTableTask.flush();
- assert.isTrue(element._renderDiffTable.called);
+ element.renderDiffTableTask?.flush();
+ assert.isTrue(stub.called);
stub.reset();
const newPrefs2 = {...newPrefs1};
delete newPrefs2.line_wrapping;
element.prefs = newPrefs2;
- element.renderDiffTableTask.flush();
- assert.isTrue(element._renderDiffTable.called);
+ element.renderDiffTableTask?.flush();
+ assert.isTrue(stub.called);
});
- test('change in preferences does not re-renders diff with ' +
- 'noRenderOnPrefsChange', () => {
- sinon.stub(element, '_renderDiffTable');
- element.noRenderOnPrefsChange = true;
- element.prefs = {
- ...MINIMAL_PREFS, time_format: 'HHMM_12'};
- element.renderDiffTableTask.flush();
- assert.isFalse(element._renderDiffTable.called);
- });
+ test(
+ 'change in preferences does not re-renders diff with ' +
+ 'noRenderOnPrefsChange',
+ () => {
+ const stub = sinon.stub(element, '_renderDiffTable');
+ element.noRenderOnPrefsChange = true;
+ element.prefs = {
+ ...MINIMAL_PREFS,
+ context: 12,
+ };
+ element.renderDiffTableTask?.flush();
+ assert.isFalse(stub.called);
+ }
+ );
});
});
@@ -732,8 +763,7 @@
element = basicFixture.instantiate();
element.diff = {
meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
- lines: 560},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
diff_header: [],
intraline_status: 'OK',
change_type: 'MODIFIED',
@@ -755,11 +785,12 @@
assert.equal(element._diffHeaderItems.length, 1);
flush();
- assert.equal(element.$.diffHeader.textContent.trim(), 'test');
+ const header = queryAndAssert(element, '#diffHeader');
+ assert.equal(header.textContent?.trim(), 'test');
});
test('binary files', () => {
- element.diff.binary = true;
+ element.diff!.binary = true;
assert.equal(element._diffHeaderItems.length, 0);
element.push('diff.diff_header', 'diff --git a/test.jpg b/test.jpg');
assert.equal(element._diffHeaderItems.length, 0);
@@ -771,16 +802,17 @@
});
suite('safety and bypass', () => {
- let renderStub;
+ let renderStub: sinon.SinonStub;
setup(() => {
element = basicFixture.instantiate();
- renderStub = sinon.stub(element.$.diffBuilder, 'render').callsFake(
- () => {
- element.$.diffBuilder.dispatchEvent(
- new CustomEvent('render', {bubbles: true, composed: true}));
- return Promise.resolve({});
- });
+ renderStub = sinon.stub(element.diffBuilder, 'render').callsFake(() => {
+ const diffTable = element.$.diffTable;
+ diffTable.dispatchEvent(
+ new CustomEvent('render', {bubbles: true, composed: true})
+ );
+ return Promise.resolve({});
+ });
sinon.stub(element, 'getDiffLength').returns(10000);
element.diff = createDiff();
element.noRenderOnPrefsChange = true;
@@ -838,7 +870,7 @@
assert.equal(element.prefs.context, 3);
assert.equal(element._safetyBypass, -1);
- assert.equal(element.$.diffBuilder.prefs.context, -1);
+ assert.equal(element.diffBuilder.prefs.context, -1);
});
test('toggles collapse context from bypass', async () => {
@@ -851,7 +883,7 @@
assert.equal(element.prefs.context, 3);
assert.isNull(element._safetyBypass);
- assert.equal(element.$.diffBuilder.prefs.context, 3);
+ assert.equal(element.diffBuilder.prefs.context, 3);
});
test('toggles collapse context from pref using default', async () => {
@@ -863,7 +895,7 @@
assert.equal(element.prefs.context, -1);
assert.equal(element._safetyBypass, 10);
- assert.equal(element.$.diffBuilder.prefs.context, 10);
+ assert.equal(element.diffBuilder.prefs.context, 10);
});
});
@@ -874,7 +906,7 @@
test('unsetting', () => {
element.blame = [];
- const setBlameSpy = sinon.spy(element.$.diffBuilder, 'setBlame');
+ const setBlameSpy = sinon.spy(element.diffBuilder, 'setBlame');
element.classList.add('showBlame');
element.blame = null;
assert.isTrue(setBlameSpy.calledWithExactly(null));
@@ -882,7 +914,15 @@
});
test('setting', () => {
- element.blame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
+ element.blame = [
+ {
+ author: 'test-author',
+ time: 12345,
+ commit_msg: '',
+ id: 'commit id',
+ ranges: [{start: 1, end: 2}],
+ },
+ ];
assert.isTrue(element.classList.contains('showBlame'));
});
});
@@ -891,8 +931,10 @@
const NO_NEWLINE_LEFT = 'No newline at end of left file.';
const NO_NEWLINE_RIGHT = 'No newline at end of right file.';
- const getWarning = element =>
- element.shadowRoot.querySelector('.newlineWarning').textContent;
+ const getWarning = (element: GrDiff) => {
+ const warningElement = queryAndAssert(element, '.newlineWarning');
+ return warningElement.textContent;
+ };
setup(() => {
element = basicFixture.instantiate();
@@ -904,8 +946,9 @@
element.showNewlineWarningLeft = true;
element.showNewlineWarningRight = true;
assert.include(
- getWarning(element),
- NO_NEWLINE_LEFT + ' \u2014 ' + NO_NEWLINE_RIGHT);// \u2014 - '—'
+ getWarning(element),
+ NO_NEWLINE_LEFT + ' \u2014 ' + NO_NEWLINE_RIGHT
+ ); // \u2014 - '—'
});
suite('showNewlineWarningLeft', () => {
@@ -918,11 +961,6 @@
element.showNewlineWarningLeft = false;
assert.notInclude(getWarning(element), NO_NEWLINE_LEFT);
});
-
- test('hide warning if undefined', () => {
- element.showNewlineWarningLeft = undefined;
- assert.notInclude(getWarning(element), NO_NEWLINE_LEFT);
- });
});
suite('showNewlineWarningRight', () => {
@@ -935,49 +973,25 @@
element.showNewlineWarningRight = false;
assert.notInclude(getWarning(element), NO_NEWLINE_RIGHT);
});
-
- test('hide warning if undefined', () => {
- element.showNewlineWarningRight = undefined;
- assert.notInclude(getWarning(element), NO_NEWLINE_RIGHT);
- });
});
test('_computeNewlineWarningClass', () => {
const hidden = 'newlineWarning hidden';
const shown = 'newlineWarning';
- assert.equal(element._computeNewlineWarningClass(null, true), hidden);
- assert.equal(element._computeNewlineWarningClass('foo', true), hidden);
- assert.equal(element._computeNewlineWarningClass(null, false), hidden);
- assert.equal(element._computeNewlineWarningClass('foo', false), shown);
- });
-
- test('_prefsEqual', () => {
- element = basicFixture.instantiate();
- assert.isTrue(element._prefsEqual(null, null));
- assert.isTrue(element._prefsEqual({}, {}));
- assert.isTrue(element._prefsEqual({x: 1}, {x: 1}));
- assert.isTrue(
- element._prefsEqual({x: 1, abc: 'def'}, {x: 1, abc: 'def'}));
- const somePref = {abc: 'def', p: true};
- assert.isTrue(element._prefsEqual(somePref, somePref));
-
- assert.isFalse(element._prefsEqual({}, null));
- assert.isFalse(element._prefsEqual(null, {}));
- assert.isFalse(element._prefsEqual({x: 1}, {x: 2}));
- assert.isFalse(element._prefsEqual({x: 1, y: 'abc'}, {x: 1, y: 'abcd'}));
- assert.isFalse(element._prefsEqual({x: 1, y: 'abc'}, {x: 1}));
- assert.isFalse(element._prefsEqual({x: 1}, {x: 1, y: 'abc'}));
+ assert.equal(element._computeNewlineWarningClass(false, true), hidden);
+ assert.equal(element._computeNewlineWarningClass(true, true), hidden);
+ assert.equal(element._computeNewlineWarningClass(false, false), hidden);
+ assert.equal(element._computeNewlineWarningClass(true, false), shown);
});
});
suite('key locations', () => {
- let renderStub;
+ let renderStub: sinon.SinonStub;
setup(() => {
element = basicFixture.instantiate();
- element.prefs = {};
- renderStub = sinon.stub(element.$.diffBuilder, 'render')
- .returns(new Promise(() => {}));
+ element.prefs = {...MINIMAL_PREFS};
+ renderStub = sinon.stub(element.diffBuilder, 'render');
});
test('lineOfInterest is a key location', () => {
@@ -994,7 +1008,7 @@
const threadEl = document.createElement('div');
threadEl.className = 'comment-thread';
threadEl.setAttribute('diff-side', 'right');
- threadEl.setAttribute('line-num', 3);
+ threadEl.setAttribute('line-num', '3');
element.appendChild(threadEl);
flush();
@@ -1021,7 +1035,11 @@
});
});
});
- const setupSampleDiff = function(params) {
+ const setupSampleDiff = function (params: {
+ content: DiffContent[];
+ ignore_whitespace?: IgnoreWhitespaceType;
+ binary?: boolean;
+ }) {
const {ignore_whitespace, content} = params;
// binary can't be undefined, use false if not set
const binary = params.binary || false;
@@ -1039,7 +1057,6 @@
show_whitespace_errors: true,
syntax_highlighting: true,
tab_size: 8,
- theme: 'DEFAULT',
};
element.diff = {
intraline_status: 'OK',
@@ -1059,21 +1076,24 @@
};
test('clear diff table content as soon as diff changes', () => {
- const content = [{
- a: ['all work and no play make andybons a dull boy'],
- }, {
- b: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- ],
- }];
+ const content = [
+ {
+ a: ['all work and no play make andybons a dull boy'],
+ },
+ {
+ b: ['Non eram nescius, Brute, cum, quae summis ingeniis '],
+ },
+ ];
function assertDiffTableWithContent() {
- assert.isTrue(element.$.diffTable.innerText.includes(content[0].a));
+ const diffTable = element.$.diffTable;
+ assert.isTrue(diffTable.innerText.includes(content[0].a?.[0] ?? ''));
}
setupSampleDiff({content});
assertDiffTableWithContent();
- element.diff = {...element.diff};
+ element.diff = {...element.diff!};
// immediately cleaned up
- assert.equal(element.$.diffTable.innerHTML, '');
+ const diffTable = element.$.diffTable;
+ assert.equal(diffTable.innerHTML, '');
element._renderDiffTable();
flush();
// rendered again
@@ -1082,40 +1102,46 @@
suite('selection test', () => {
test('user-select set correctly on side-by-side view', () => {
- const content = [{
- a: ['all work and no play make andybons a dull boy'],
- b: ['elgoog elgoog elgoog'],
- }, {
- ab: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
- ],
- }];
+ const content = [
+ {
+ a: ['all work and no play make andybons a dull boy'],
+ b: ['elgoog elgoog elgoog'],
+ },
+ {
+ ab: [
+ 'Non eram nescius, Brute, cum, quae summis ingeniis ',
+ 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+ ],
+ },
+ ];
setupSampleDiff({content});
flush();
- const diffLine = element.shadowRoot.querySelectorAll('.contentText')[2];
+
+ const diffLine = queryAll<HTMLElement>(element, '.contentText')[2];
assert.equal(getComputedStyle(diffLine).userSelect, 'none');
- // click to mark it as selected
- MockInteractions.tap(diffLine);
+ mouseDown(diffLine);
assert.equal(getComputedStyle(diffLine).userSelect, 'text');
});
test('user-select set correctly on unified view', () => {
- const content = [{
- a: ['all work and no play make andybons a dull boy'],
- b: ['elgoog elgoog elgoog'],
- }, {
- ab: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
- ],
- }];
+ const content = [
+ {
+ a: ['all work and no play make andybons a dull boy'],
+ b: ['elgoog elgoog elgoog'],
+ },
+ {
+ ab: [
+ 'Non eram nescius, Brute, cum, quae summis ingeniis ',
+ 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+ ],
+ },
+ ];
setupSampleDiff({content});
- element.viewMode = 'UNIFIED_DIFF';
+ element.viewMode = DiffViewMode.UNIFIED;
flush();
- const diffLine = element.shadowRoot.querySelectorAll('.contentText')[2];
+ const diffLine = queryAll<HTMLElement>(element, '.contentText')[2];
assert.equal(getComputedStyle(diffLine).userSelect, 'none');
- MockInteractions.tap(diffLine);
+ mouseDown(diffLine);
assert.equal(getComputedStyle(diffLine).userSelect, 'text');
});
});
@@ -1123,71 +1149,87 @@
suite('whitespace changes only message', () => {
test('show the message if ignore_whitespace is criteria matches', () => {
setupSampleDiff({content: [{skip: 100}]});
- assert.isTrue(element.showNoChangeMessage(
+ assert.isTrue(
+ element.showNoChangeMessage(
/* loading= */ false,
element.prefs,
element._diffLength,
element.diff
- ));
+ )
+ );
});
test('do not show the message for binary files', () => {
setupSampleDiff({content: [{skip: 100}], binary: true});
- assert.isFalse(element.showNoChangeMessage(
+ assert.isFalse(
+ element.showNoChangeMessage(
/* loading= */ false,
element.prefs,
element._diffLength,
element.diff
- ));
+ )
+ );
});
test('do not show the message if still loading', () => {
setupSampleDiff({content: [{skip: 100}]});
- assert.isFalse(element.showNoChangeMessage(
+ assert.isFalse(
+ element.showNoChangeMessage(
/* loading= */ true,
element.prefs,
element._diffLength,
element.diff
- ));
+ )
+ );
});
test('do not show the message if contains valid changes', () => {
- const content = [{
- a: ['all work and no play make andybons a dull boy'],
- b: ['elgoog elgoog elgoog'],
- }, {
- ab: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
- ],
- }];
+ const content = [
+ {
+ a: ['all work and no play make andybons a dull boy'],
+ b: ['elgoog elgoog elgoog'],
+ },
+ {
+ ab: [
+ 'Non eram nescius, Brute, cum, quae summis ingeniis ',
+ 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+ ],
+ },
+ ];
setupSampleDiff({content});
assert.equal(element._diffLength, 3);
- assert.isFalse(element.showNoChangeMessage(
+ assert.isFalse(
+ element.showNoChangeMessage(
/* loading= */ false,
element.prefs,
element._diffLength,
element.diff
- ));
+ )
+ );
});
test('do not show message if ignore whitespace is disabled', () => {
- const content = [{
- a: ['all work and no play make andybons a dull boy'],
- b: ['elgoog elgoog elgoog'],
- }, {
- ab: [
- 'Non eram nescius, Brute, cum, quae summis ingeniis ',
- 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
- ],
- }];
+ const content = [
+ {
+ a: ['all work and no play make andybons a dull boy'],
+ b: ['elgoog elgoog elgoog'],
+ },
+ {
+ ab: [
+ 'Non eram nescius, Brute, cum, quae summis ingeniis ',
+ 'exquisitaque doctrina philosophi Graeco sermone tractavissent',
+ ],
+ },
+ ];
setupSampleDiff({ignore_whitespace: 'IGNORE_NONE', content});
- assert.isFalse(element.showNoChangeMessage(
+ assert.isFalse(
+ element.showNoChangeMessage(
/* loading= */ false,
element.prefs,
element._diffLength,
element.diff
- ));
+ )
+ );
});
});
@@ -1195,21 +1237,4 @@
const diff = createDiff();
assert.equal(element.getDiffLength(diff), 52);
});
-
- test('_prefsEqual', () => {
- element = basicFixture.instantiate();
- assert.isTrue(element._prefsEqual(null, null));
- assert.isTrue(element._prefsEqual({}, {}));
- assert.isTrue(element._prefsEqual({x: 1}, {x: 1}));
- assert.isTrue(element._prefsEqual({x: 1, abc: 'def'}, {x: 1, abc: 'def'}));
- const somePref = {abc: 'def', p: true};
- assert.isTrue(element._prefsEqual(somePref, somePref));
-
- assert.isFalse(element._prefsEqual({}, null));
- assert.isFalse(element._prefsEqual(null, {}));
- assert.isFalse(element._prefsEqual({x: 1}, {x: 2}));
- assert.isFalse(element._prefsEqual({x: 1, y: 'abc'}, {x: 1, y: 'abcd'}));
- assert.isFalse(element._prefsEqual({x: 1, y: 'abc'}, {x: 1}));
- assert.isFalse(element._prefsEqual({x: 1}, {x: 1, y: 'abc'}));
- });
});
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
deleted file mode 100644
index bb46484..0000000
--- a/polygerrit-ui/app/embed/gr-diff-app-context-init_test.js
+++ /dev/null
@@ -1,32 +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 {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);
- const service2 = appContext[serviceName];
- assert.strictEqual(service, service2);
- });
- });
-});
-
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init_test.ts b/polygerrit-ui/app/embed/gr-diff-app-context-init_test.ts
new file mode 100644
index 0000000..84fd859
--- /dev/null
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init_test.ts
@@ -0,0 +1,22 @@
+/**
+ * @license
+ * Copyright 2020 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {AppContext} from '../services/app-context';
+import '../test/common-test-setup-karma';
+import {createDiffAppContext} from './gr-diff-app-context-init';
+
+suite('gr diff app context initializer tests', () => {
+ test('all services initialized and are singletons', () => {
+ const appContext: AppContext = createDiffAppContext();
+ for (const serviceName of Object.keys(appContext) as Array<
+ keyof AppContext
+ >) {
+ const service = appContext[serviceName];
+ assert.isNotNull(service);
+ const service2 = appContext[serviceName];
+ assert.strictEqual(service, service2);
+ }
+ });
+});
diff --git a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
index c276f79..a34f880 100644
--- a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
+++ b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model.ts
@@ -9,14 +9,21 @@
NumericChangeId,
ChangeStatus,
ReviewerState,
+ AccountId,
AccountInfo,
+ GroupInfo,
} from '../../api/rest-api';
import {Model} from '../model';
import {Finalizable} from '../../services/registry';
import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
import {define} from '../dependency';
import {select} from '../../utils/observable-util';
-import {ReviewInput, ReviewerInput} from '../../types/common';
+import {
+ ReviewInput,
+ ReviewerInput,
+ AttentionSetInput,
+} from '../../types/common';
+import {accountOrGroupKey} from '../../utils/account-util';
export const bulkActionsModelToken =
define<BulkActionsModel>('bulk-actions-model');
@@ -154,14 +161,15 @@
}
addReviewers(
- changedReviewers: Map<ReviewerState, AccountInfo[]>
+ changedReviewers: Map<ReviewerState, (AccountInfo | GroupInfo)[]>,
+ reason: string
): Promise<Response>[] {
const current = this.subject$.getValue();
const changes = current.selectedChangeNums.map(
changeNum => current.allChanges.get(changeNum)!
);
return changes.map(change => {
- const reviewersNewToChange = [
+ const reviewersNewToChange: ReviewerInput[] = [
ReviewerState.REVIEWER,
ReviewerState.CC,
].flatMap(state =>
@@ -170,8 +178,20 @@
if (reviewersNewToChange.length === 0) {
return Promise.resolve(new Response());
}
+ const attentionSetUpdates: AttentionSetInput[] = reviewersNewToChange
+ .filter(reviewerInput => reviewerInput.state === ReviewerState.REVIEWER)
+ .map(reviewerInput => {
+ return {
+ // TODO: Once Groups are supported, filter them out and only add
+ // Accounts to the attention set, just like gr-reply-dialog.
+ user: reviewerInput.reviewer as AccountId,
+ reason,
+ };
+ });
const reviewInput: ReviewInput = {
reviewers: reviewersNewToChange,
+ ignore_automatic_attention_set_rules: true,
+ add_to_attention_set: attentionSetUpdates,
};
return this.restApiService.saveChangeReview(
change._number,
@@ -242,14 +262,14 @@
private getNewReviewersToChange(
change: ChangeInfo,
state: ReviewerState,
- changedReviewers: Map<ReviewerState, AccountInfo[]>
+ changedReviewers: Map<ReviewerState, (AccountInfo | GroupInfo)[]>
): ReviewerInput[] {
return (
changedReviewers
.get(state)
?.filter(account => !change.reviewers[state]?.includes(account))
.map(account => {
- return {state, reviewer: account._account_id!};
+ return {state, reviewer: accountOrGroupKey(account)};
}) ?? []
);
}
diff --git a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
index 5347b41..84d5c4e 100644
--- a/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
+++ b/polygerrit-ui/app/models/bulk-actions/bulk-actions-model_test.ts
@@ -7,6 +7,7 @@
import {
createAccountWithIdNameAndEmail,
createChange,
+ createGroupInfo,
createRevisions,
} from '../../test/test-data-generators';
import {
@@ -18,6 +19,7 @@
AccountInfo,
ReviewerState,
AccountId,
+ GroupInfo,
} from '../../api/rest-api';
import {BulkActionsModel, LoadingState} from './bulk-actions-model';
import {getAppContext} from '../../services/app-context';
@@ -200,6 +202,7 @@
createAccountWithIdNameAndEmail(0),
createAccountWithIdNameAndEmail(1),
];
+ const groups: GroupInfo[] = [createGroupInfo('groupId')];
const changes: ChangeInfo[] = [
{
...createChange(),
@@ -234,21 +237,49 @@
test('adds reviewers/cc only to changes that need it', async () => {
bulkActionsModel.addReviewers(
new Map([
- [ReviewerState.REVIEWER, [accounts[0]]],
+ [ReviewerState.REVIEWER, [accounts[0], groups[0]]],
[ReviewerState.CC, [accounts[1]]],
- ])
+ ]),
+ '<GERRIT_ACCOUNT_12345> replied on the change'
);
- // changes[0] is not updated since it already has the reviewer & CC
- assert.isTrue(saveChangeReviewStub.calledOnce);
+ assert.isTrue(saveChangeReviewStub.calledTwice);
+ // changes[0] only adds the group since it already has the other
+ // reviewer/CCs
assert.sameDeepOrderedMembers(saveChangeReviewStub.firstCall.args, [
+ changes[0]._number,
+ 'current',
+ {
+ reviewers: [{reviewer: groups[0].id, state: ReviewerState.REVIEWER}],
+ ignore_automatic_attention_set_rules: true,
+ add_to_attention_set: [
+ {
+ reason: '<GERRIT_ACCOUNT_12345> replied on the change',
+ user: groups[0].id,
+ },
+ ],
+ },
+ ]);
+ assert.sameDeepOrderedMembers(saveChangeReviewStub.secondCall.args, [
changes[1]._number,
'current',
{
reviewers: [
{reviewer: accounts[0]._account_id, state: ReviewerState.REVIEWER},
+ {reviewer: groups[0].id, state: ReviewerState.REVIEWER},
{reviewer: accounts[1]._account_id, state: ReviewerState.CC},
],
+ ignore_automatic_attention_set_rules: true,
+ add_to_attention_set: [
+ {
+ reason: '<GERRIT_ACCOUNT_12345> replied on the change',
+ user: accounts[0]._account_id,
+ },
+ {
+ reason: '<GERRIT_ACCOUNT_12345> replied on the change',
+ user: groups[0].id,
+ },
+ ],
},
]);
});
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index a1b732f..44d63d4b 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -28,7 +28,6 @@
export enum KnownExperimentId {
NEW_IMAGE_DIFF_UI = 'UiFeature__new_image_diff_ui',
CHECKS_DEVELOPER = 'UiFeature__checks_developer',
- SUBMIT_REQUIREMENTS_UI = 'UiFeature__submit_requirements_ui',
BULK_ACTIONS = 'UiFeature__bulk_actions_dashboard',
DIFF_RENDERING_LIT = 'UiFeature__diff_rendering_lit',
}
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 5f77e8a..028b2af 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
@@ -62,7 +62,7 @@
static CREDS_EXPIRED_MSG = 'Credentials expired.';
- private authCheckPromise?: Promise<Response>;
+ private authCheckPromise?: Promise<boolean>;
private _last_auth_check_time: number = Date.now();
@@ -100,37 +100,37 @@
Date.now() - this._last_auth_check_time > MAX_AUTH_CHECK_WAIT_TIME_MS
) {
// Refetch after last check expired
- this.authCheckPromise = fetch(`${this.baseUrl}/auth-check`);
+ this.authCheckPromise = fetch(`${this.baseUrl}/auth-check`)
+ .then(res => {
+ // Make a call that requires loading the body of the request. This makes it so that the browser
+ // can close the request even though callers of this method might only ever read headers.
+ // See https://stackoverflow.com/questions/45816743/how-to-solve-this-caution-request-is-not-finished-yet-in-chrome
+ try {
+ res.clone().text();
+ } catch {
+ // Ignore error
+ }
+
+ // auth-check will return 204 if authed
+ // treat the rest as unauthed
+ if (res.status === 204) {
+ this._setStatus(Auth.STATUS.AUTHED);
+ return true;
+ } else {
+ this._setStatus(Auth.STATUS.NOT_AUTHED);
+ return false;
+ }
+ })
+ .catch(() => {
+ this._setStatus(AuthStatus.ERROR);
+ // Reset authCheckPromise to avoid caching the failed promise
+ this.authCheckPromise = undefined;
+ return false;
+ });
this._last_auth_check_time = Date.now();
}
- return this.authCheckPromise
- .then(res => {
- // Make a call that requires loading the body of the request. This makes it so that the browser
- // can close the request even though callers of this method might only ever read headers.
- // See https://stackoverflow.com/questions/45816743/how-to-solve-this-caution-request-is-not-finished-yet-in-chrome
- try {
- res.clone().text();
- } catch {
- // Ignore error
- }
-
- // auth-check will return 204 if authed
- // treat the rest as unauthed
- if (res.status === 204) {
- this._setStatus(Auth.STATUS.AUTHED);
- return true;
- } else {
- this._setStatus(Auth.STATUS.NOT_AUTHED);
- return false;
- }
- })
- .catch(() => {
- this._setStatus(AuthStatus.ERROR);
- // Reset authCheckPromise to avoid caching the failed promise
- this.authCheckPromise = undefined;
- return false;
- });
+ return this.authCheckPromise;
}
clearCache() {
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
index 2b4fc60..3193833 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
@@ -2702,20 +2702,22 @@
return Promise.all([promiseA, promiseB]).then(results => {
// Sometimes the server doesn't send back the content type.
- const baseImage: Base64ImageFile | null = results[0]
- ? {
- ...results[0],
- _expectedType: diff.meta_a.content_type,
- _name: diff.meta_a.name,
- }
- : null;
- const revisionImage: Base64ImageFile | null = results[1]
- ? {
- ...results[1],
- _expectedType: diff.meta_b.content_type,
- _name: diff.meta_b.name,
- }
- : null;
+ const baseImage: Base64ImageFile | null =
+ results[0] && diff.meta_a
+ ? {
+ ...results[0],
+ _expectedType: diff.meta_a.content_type,
+ _name: diff.meta_a.name,
+ }
+ : null;
+ const revisionImage: Base64ImageFile | null =
+ results[1] && diff.meta_b
+ ? {
+ ...results[1],
+ _expectedType: diff.meta_b.content_type,
+ _name: diff.meta_b.name,
+ }
+ : null;
const imagesForDiff: ImagesForDiff = {baseImage, revisionImage};
return imagesForDiff;
});
diff --git a/polygerrit-ui/app/services/registry.ts b/polygerrit-ui/app/services/registry.ts
index e7de1ef..74b6997 100644
--- a/polygerrit-ui/app/services/registry.ts
+++ b/polygerrit-ui/app/services/registry.ts
@@ -73,7 +73,7 @@
initializing = true;
initialized.set(name, factory(context));
} catch (e) {
- console.error(`Failed to initialize ${name}`, e);
+ console.error(`Failed to initialize ${String(name)}`, e);
} finally {
initializing = false;
}
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index dc17ee6..97e0c01 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -372,9 +372,20 @@
/* diff colors */
--dark-add-highlight-color: #aaf2aa;
- --dark-rebased-add-highlight-color: #d7d7f9;
- --dark-rebased-remove-highlight-color: #f7e8b7;
+ --light-add-highlight-color: #d8fed8;
--dark-remove-highlight-color: #ffcdd2;
+ --light-remove-highlight-color: #ffebee;
+
+ --dark-rebased-add-highlight-color: #d7d7f9;
+ --light-rebased-add-highlight-color: #eef;
+ --dark-rebased-remove-highlight-color: #f7e8b7;
+ --light-rebased-remove-highlight-color: #fff8dc;
+
+ --diff-moved-in-background: var(--cyan-50);
+ --diff-moved-in-label-color: var(--cyan-900);
+ --diff-moved-out-background: var(--purple-50);
+ --diff-moved-out-label-color: var(--purple-900);
+
--diff-blank-background-color: var(--background-color-secondary);
--diff-context-control-background-color: #fff7d4;
--diff-context-control-border-color: #f6e6a5;
@@ -385,14 +396,6 @@
--diff-tab-indicator-color: var(--deemphasized-text-color);
--diff-trailing-whitespace-indicator: #ff9ad2;
--focused-line-outline-color: var(--blue-700);
- --light-add-highlight-color: #d8fed8;
- --light-rebased-add-highlight-color: #eef;
- --diff-moved-in-background: var(--cyan-50);
- --diff-moved-out-background: var(--purple-50);
- --diff-moved-in-label-color: var(--cyan-900);
- --diff-moved-out-label-color: var(--purple-900);
- --light-remove-add-highlight-color: #fff8dc;
- --light-remove-highlight-color: #ffebee;
--coverage-covered: #e0f2f1;
--coverage-not-covered: #ffd1a4;
--ranged-comment-hint-text-color: var(--orange-900);
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index 6f19924..17c967d 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -207,9 +207,20 @@
/* diff colors */
--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);
+ --light-add-highlight-color: #182b1f;
--dark-remove-highlight-color: #62110f;
+ --light-remove-highlight-color: #320404;
+
+ --dark-rebased-add-highlight-color: rgba(11, 255, 155, 0.15);
+ --light-rebased-add-highlight-color: #487165;
+ --dark-rebased-remove-highlight-color: rgba(255, 139, 6, 0.15);
+ --light-rebased-remove-highlight-color: #2f3f2f;
+
+ --diff-moved-in-background: #1d4042;
+ --diff-moved-in-label-color: var(--cyan-50);
+ --diff-moved-out-background: #230e34;
+ --diff-moved-out-label-color: var(--purple-50);
+
--diff-blank-background-color: var(--background-color-secondary);
--diff-context-control-background-color: #333311;
--diff-context-control-border-color: var(--border-color);
@@ -220,14 +231,6 @@
--diff-tab-indicator-color: var(--deemphasized-text-color);
--diff-trailing-whitespace-indicator: #ff9ad2;
--focused-line-outline-color: var(--blue-200);
- --light-add-highlight-color: #182b1f;
- --light-rebased-add-highlight-color: #487165;
- --diff-moved-in-background: #1d4042;
- --diff-moved-out-background: #230e34;
- --diff-moved-in-label-color: var(--cyan-50);
- --diff-moved-out-label-color: var(--purple-50);
- --light-remove-add-highlight-color: #2f3f2f;
- --light-remove-highlight-color: #320404;
--coverage-covered: #112826;
--coverage-not-covered: #6b3600;
--ranged-comment-hint-text-color: var(--blue-50);
diff --git a/polygerrit-ui/app/test/mocks/comment-api.js b/polygerrit-ui/app/test/mocks/comment-api.js
deleted file mode 100644
index fc4599d..0000000
--- a/polygerrit-ui/app/test/mocks/comment-api.js
+++ /dev/null
@@ -1,47 +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 {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-
-/**
- * This is an "abstract" class for tests. The descendant must define a template
- * for this element and a tagName - see createCommentApiMockWithTemplateElement below
- */
-class CommentApiMock extends LegacyElementMixin(PolymerElement) {
- static get properties() {
- return {
- _changeComments: Object,
- };
- }
-}
-
-/**
- * Creates a new element which is descendant of CommentApiMock with specified
- * template. Additionally, the method registers a tagName for this element.
- *
- * Each tagName must be a unique accross all tests.
- */
-export function createCommentApiMockWithTemplateElement(tagName, template) {
- const elementClass = class extends CommentApiMock {
- static get is() { return tagName; }
-
- static get template() { return template; }
- };
- customElements.define(tagName, elementClass);
- return elementClass;
-}
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 829a36b..e90ad2c 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -22,6 +22,7 @@
ApprovalInfo,
AuthInfo,
BasePatchSetNum,
+ BlameInfo,
BranchName,
ChangeConfigInfo,
ChangeId,
@@ -54,6 +55,7 @@
MaxObjectSizeLimitInfo,
MergeableInfo,
NumericChangeId,
+ PatchRange,
PatchSetNum,
PluginConfigInfo,
PreferencesInfo,
@@ -64,6 +66,7 @@
RequirementType,
Reviewers,
RevisionInfo,
+ RevisionPatchSetNum,
RobotCommentInfo,
RobotId,
RobotRunId,
@@ -258,6 +261,16 @@
};
}
+export function createPatchRange(
+ basePatchNum?: number,
+ patchNum?: number
+): PatchRange {
+ return {
+ basePatchNum: (basePatchNum ?? 'PARENT') as BasePatchSetNum,
+ patchNum: (patchNum ?? 1) as RevisionPatchSetNum,
+ };
+}
+
export function createRevision(
patchSetNum = 1,
description = ''
@@ -469,6 +482,24 @@
};
}
+export function createEmptyDiff(): DiffInfo {
+ return {
+ meta_a: {
+ name: 'empty-left.txt',
+ content_type: 'text/plain',
+ lines: 1,
+ },
+ meta_b: {
+ name: 'empty-right.txt',
+ content_type: 'text/plain',
+ lines: 1,
+ },
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ content: [],
+ };
+}
+
export function createDiff(): DiffInfo {
return {
meta_a: {
@@ -585,6 +616,16 @@
};
}
+export function createBlame(): BlameInfo {
+ return {
+ author: 'test-author',
+ id: 'test-id',
+ time: 123,
+ commit_msg: 'test-commit-message',
+ ranges: [],
+ };
+}
+
export function createMergeable(): MergeableInfo {
return {
submit_type: SubmitType.MERGE_IF_NECESSARY,
@@ -825,7 +866,9 @@
};
}
-export function createCommentThread(comments: Array<Partial<CommentInfo>>) {
+export function createCommentThread(
+ comments: Array<Partial<CommentInfo | DraftInfo>>
+) {
if (!comments.length) {
throw new Error('comment is required to create a thread');
}
diff --git a/polygerrit-ui/app/types/diff.ts b/polygerrit-ui/app/types/diff.ts
index 562d47f..7ad656d 100644
--- a/polygerrit-ui/app/types/diff.ts
+++ b/polygerrit-ui/app/types/diff.ts
@@ -48,9 +48,9 @@
export interface DiffInfo extends DiffInfoApi {
/** Meta information about the file on side A as a DiffFileMetaInfo entity. */
- meta_a: DiffFileMetaInfo;
+ meta_a?: DiffFileMetaInfo;
/** Meta information about the file on side B as a DiffFileMetaInfo entity. */
- meta_b: DiffFileMetaInfo;
+ meta_b?: DiffFileMetaInfo;
/**
* Links to the file diff in external sites as a list of DiffWebLinkInfo
diff --git a/polygerrit-ui/app/utils/account-util.ts b/polygerrit-ui/app/utils/account-util.ts
index b7cc77b..b6018ba 100644
--- a/polygerrit-ui/app/utils/account-util.ts
+++ b/polygerrit-ui/app/utils/account-util.ts
@@ -35,7 +35,7 @@
export const ACCOUNT_TEMPLATE_REGEX = '<GERRIT_ACCOUNT_(\\d+)>';
export function accountKey(account: AccountInfo): AccountId | EmailAddress {
- if (account._account_id) return account._account_id;
+ if (account._account_id !== undefined) return account._account_id;
if (account.email) return account.email;
throw new Error('Account has neither _account_id nor email.');
}
diff --git a/polygerrit-ui/app/utils/common-util.ts b/polygerrit-ui/app/utils/common-util.ts
index 6ccf770..95b753c 100644
--- a/polygerrit-ui/app/utils/common-util.ts
+++ b/polygerrit-ui/app/utils/common-util.ts
@@ -159,7 +159,8 @@
/**
* Returns the elements that are present in every sub-array. If a compareBy
- * predicate is passed in, it will be used instead of strict equality.
+ * predicate is passed in, it will be used instead of strict equality. A new
+ * array is always returned even if there is already just a single array.
*/
export function intersection<T>(
arrays: T[][],
@@ -171,6 +172,9 @@
if (arrays.length === 0) {
return [];
}
+ if (arrays.length === 1) {
+ return [...arrays[0]];
+ }
return arrays.reduce((result, array) =>
result.filter(t => array.find(u => compareBy(t, u)))
);
diff --git a/polygerrit-ui/app/utils/common-util_test.ts b/polygerrit-ui/app/utils/common-util_test.ts
index 8cc523a..0adfaa6 100644
--- a/polygerrit-ui/app/utils/common-util_test.ts
+++ b/polygerrit-ui/app/utils/common-util_test.ts
@@ -75,8 +75,11 @@
});
test('intersections', () => {
+ const arrayWithValues = [1, 2, 3];
assert.sameDeepMembers(intersection([]), []);
- assert.sameDeepMembers(intersection([[1, 2, 3]]), [1, 2, 3]);
+ assert.sameDeepMembers(intersection([arrayWithValues]), arrayWithValues);
+ // a new array is returned even if a single array is provided.
+ assert.notStrictEqual(intersection([arrayWithValues]), arrayWithValues);
assert.sameDeepMembers(
intersection([
[1, 2, 3],
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index 16e0586..be6aa06 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -491,8 +491,18 @@
return false;
}
+/** Returns a promise that waits for the element's height to become > 0. */
+export function untilRendered(el: HTMLElement) {
+ return new Promise(resolve => {
+ whenRendered(el, resolve);
+ });
+}
+
/** Executes the given callback when the element's height is > 0. */
-export function whenRendered(el: HTMLElement, callback: () => void) {
+export function whenRendered(
+ el: HTMLElement,
+ callback: (value?: unknown) => void
+) {
if (el.clientHeight > 0) {
callback();
return;
diff --git a/polygerrit-ui/app/utils/event-util.ts b/polygerrit-ui/app/utils/event-util.ts
index 418adbd..e624cef 100644
--- a/polygerrit-ui/app/utils/event-util.ts
+++ b/polygerrit-ui/app/utils/event-util.ts
@@ -32,7 +32,7 @@
);
}
-type HTMLElementEventDetailType<K extends keyof HTMLElementEventMap> =
+export type HTMLElementEventDetailType<K extends keyof HTMLElementEventMap> =
HTMLElementEventMap[K] extends CustomEvent<infer DT>
? unknown extends DT
? never
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index f5703f4..2b6f700 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -21,7 +21,6 @@
SubmitRequirementStatus,
LabelNameToValuesMap,
} from '../api/rest-api';
-import {FlagsService, KnownExperimentId} from '../services/flags/flags';
import {
AccountInfo,
ApprovalInfo,
@@ -421,16 +420,3 @@
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/tools/js/template_checker.bzl b/tools/js/template_checker.bzl
index da77234..6c645f4 100644
--- a/tools/js/template_checker.bzl
+++ b/tools/js/template_checker.bzl
@@ -123,9 +123,7 @@
)
# Pack all transformed files. Later files can be materialized in the
- # WORKSPACE/polygerrit-ui/app/tmpl_out dir. The following command do it
- # automatically
- # npm run polytest:dev
+ # WORKSPACE/polygerrit-ui/app/tmpl_out dir.
pkg_tar(
name = name + "_tar",
srcs = generated_dev_files,
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index d8f7020..37d8b9c 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -243,36 +243,36 @@
sha1 = "64cba89cf87c1d84cb8c81d06f0b9c482f10b4dc",
)
- LUCENE_VERS = "6.6.5"
+ LUCENE_VERS = "7.7.3"
maven_jar(
name = "lucene-core",
artifact = "org.apache.lucene:lucene-core:" + LUCENE_VERS,
- sha1 = "2983f80b1037e098209657b0ca9176827892d0c0",
+ sha1 = "5faa5ae56f7599019fce6184accc6c968b7519e7",
)
maven_jar(
name = "lucene-analyzers-common",
artifact = "org.apache.lucene:lucene-analyzers-common:" + LUCENE_VERS,
- sha1 = "6094f91071d90570b7f5f8ce481d5de7d2d2e9d5",
+ sha1 = "0a76cbf5e21bbbb0c2d6288b042450236248214e",
)
maven_jar(
name = "backward-codecs",
artifact = "org.apache.lucene:lucene-backward-codecs:" + LUCENE_VERS,
- sha1 = "460a19e8d1aa7d31e9614cf528a6cb508c9e823d",
+ sha1 = "40207d0dd023a0e2868a23dd87d72f1a3cdbb893",
)
maven_jar(
name = "lucene-misc",
artifact = "org.apache.lucene:lucene-misc:" + LUCENE_VERS,
- sha1 = "ce3a1b7b6a92b9af30791356a4bd46d1cea6cc1e",
+ sha1 = "3aca078edf983059722fe61a81b7b7bd5ecdb222",
)
maven_jar(
name = "lucene-queryparser",
artifact = "org.apache.lucene:lucene-queryparser:" + LUCENE_VERS,
- sha1 = "2db9ca0086a4b8e0b9bc9f08a9b420303168e37c",
+ sha1 = "685fc6166d29eb3e3441ae066873bb442aa02df1",
)
# JGit's transitive dependencies