Merge "Convert gr-app_test to typescript"
diff --git a/Documentation/concept-changes.txt b/Documentation/concept-changes.txt
index 6de787c..0444fab 100644
--- a/Documentation/concept-changes.txt
+++ b/Documentation/concept-changes.txt
@@ -104,6 +104,9 @@
Assigning a topic to a change can be done in the change screen or through a `git
push` command.
+For more information about using topics, see the user guide:
+link:cross-repository-changes.html[Submitting Changes Across Repositories by using Topics].
+
[[submit-strategies]]
== Submit strategies
diff --git a/Documentation/config-validation.txt b/Documentation/config-validation.txt
index cb953c1..56c9ecd 100644
--- a/Documentation/config-validation.txt
+++ b/Documentation/config-validation.txt
@@ -21,6 +21,14 @@
Out of the box, Gerrit includes a plugin that checks the length of the
subject and body lines of commit messages on uploaded commits.
+[plugin-push-options]]
+=== Plugin push options
+
+Plugins can register push options by implementing the `PluginPushOption`
+interface. If a plugin push option was specified it is available from
+the `CommitReceivedEvent` that is passed into `CommitValidationListener`.
+This way the plugin commit validation can be controlled by push options.
+
[[user-ref-operations-validation]]
== User ref operations validation
diff --git a/Documentation/cross-repository-changes.txt b/Documentation/cross-repository-changes.txt
new file mode 100644
index 0000000..136219c
--- /dev/null
+++ b/Documentation/cross-repository-changes.txt
@@ -0,0 +1,252 @@
+:linkattrs:
+= Gerrit Code Review - Submitting Changes Across Repositories by using Topics
+
+== Goal
+
+This document describes how to propose and submit code changes across multiple
+Git repositories together in Gerrit.
+
+== When to Use
+
+Oftentimes, especially for larger code bases, code is split across multiple
+repositories. The Android operating system’s code base, for example, consists of
+https://android.googlesource.com/[hundreds] of separate repositories. When
+making a change, you might make code changes that span multiple repositories.
+For example, one repository could define an API which is used in another
+repository. Submitting these changes across these repositories separately could
+cause the build to break for other developers.
+
+Gerrit provides a mechanism called link:intro-user.html#topics[Topics] to submit
+changes together to prevent this problem.
+
+|===
+|NOTE: Usage of topics to submit multiple changes together requires your
+Gerrit host having
+link:config-gerrit.html#change.submitWholeTopic[config.submitWholeTopic] set to
+true. Ask your Gerrit administrator if you're not sure if this is enabled for
+your Gerrit instance.
+|===
+
+== What is a Topic?
+
+* A topic is a string that can be associated with a change.
+* Multiple changes can use that topic to be submitted at the same time (assuming
+ approvals, etc.).
+* Submitting a change with a topic causes all of the changes in the topic *to be
+ submitted together*
+ ** Topics that span only a single repository are guaranteed to be submitted
+ together
+ ** Topics that span multiple repositories simply triggers submission of all
+ changes. No other guarantees are given. Submission of all changes could
+ fail, so you could get a partial topic submission. This is very rare but
+ can happen in some of the following situations:
+ *** Storage layer failures. This is unlikely in single-master installation and
+ more likely with multi-master setups.
+ *** Race conditions. Concurrent submits to the same repository or concurrent
+ updates of the pending changes.
+
+Here are a few intricacies you should be aware of:
+
+1. Topics can only be used for changes within a single Gerrit instance. There is
+no builtin support for synchronizing with other Gerrit or Git hosting sites.
+
+2. A topic can be any string, and they are not namespaced in a Gerrit instance;
+there is a chance for collisions and inadvertently grouping changes together
+that weren’t meant to be grouped. This could even happen with changes you can’t
+see, leading to more confusion e.g. (change not submittable, but you can't see
+why it's not submittable.). We suggest prefixing topic strings with the author’s
+username e.g. “username-” to help avoid this.
+
+You can view the assigned topic from the change screen in Gerrit:
+
+image::images/cross-repository-changes-topic.png[width=600]
+
+=== Topic submission behavior
+* Submitting a topic will submit any dependent changes as well. For example,
+ an unsubmitted parent change will also be submitted, even if it isn’t in the
+ original topic.
+* A change with a topic is submittable when *all changes* in the topic are
+ submittable and *all of the changes’ dependent changes* (and their topics!)
+ are also submittable.
+* Gerrit calls the totality of these changes "Submitted Together", and they can
+be found with the
+ link:rest-api-changes.html#submitted-together[Submitted Together endpoint] or
+ on the change screen.
+
+image::images/cross-repository-changes-submitted-together.png[width=600]
+
+* A submission creates a unique submission ID
+ (link:rest-api-changes.html#change-info[`submission_id`]), which can be
+ used in Gerrit's search bar to find all the submitted changes for the
+ submission. This ID is relevant when <<reverting,reverting a submission>>.
+
+To better underestand this behavior, consider this following example.
+
+==== Example Submission[[example_submission]]
+
+image::images/cross-repository-changes-example.png[width=600]
+
+* Two repositories: A and B
+* Two changes in A: A1 and A2, where A2 is the child change.
+* Two changes in B: B1 and B2, where B2 is the child change.
+* Topic X contains change A1 and B1
+* Topic Y contains change A2 and B2
+
+Submission of A2 will submit all four of these changes because submission of A2
+submits all of topic Y as well as all dependent changes and their topics i.e. A1
+and topic X.
+
+Because of this, any submission is blocked until all four of these changes are
+submittable.
+
+|===
+| Important point: B1 can unexpectedly block the submission of A2!
+This kind of situation is hard to immediately grok: B1 isn't in the topic you're
+trying to submit, and it isn't a depnedent change of A2. If your topic isn’t
+submittable and you can’t figure out why, this might be a reason.
+|===
+
+== Submitting Changes Using Topics
+
+=== 1. *Associate the changes to a topic*
+
+The first step is to associate all the changes you want to be submitted together
+with the same topic. There are multiple ways to associate changes with a topic.
+
+==== From the command line
+You can set the topic name when uploading to Gerrit
+
+----
+$ git push origin HEAD:refs/heads/master -o topic=[YOUR_TOPIC_NAME]
+----
+
+*OR*
+
+----
+$ git push origin HEAD:refs/for/master%topic=[YOUR_TOPIC_NAME]
+----
+
+If you’re using https://source.android.com/setup/develop[repo] to upload a
+change to Android Gerrit, you can associate a topic via:
+
+----
+$ repo upload -o topic=[YOUR_TOPIC_NAME]
+----
+
+If you’re using
+https://commondatastorage.googleapis.com/chrome-infra-docs/flat/depot_tools/docs/html/depot_tools.html[depot_tools]
+to upload a change to Chromium Gerrit, you can associate a topic via:
+
+----
+$ git cl upload --topic=[YOUR_TOPIC_NAME]
+----
+
+==== From the UI
+
+If the change has already been created, you can add a topic from the change page
+by clicking ADD TOPIC, found on the left side of the top of the Change screen.
+
+image::images/cross-repository-changes-add-topic.png[width=600]
+
+=== 2. *Go through the normal code review process*
+
+Each change still goes through the normal code review process where reviewers
+vote on each change individually. The changes won’t be able to be submitted
+until *all* changes in the topic are submittable.
+
+The requirements for submittability vary based on rules set by your repository
+administrators; often this includes being approved by all requisite parties,
+passing presubmit testing, and being able to merge cleanly (without conflicts)
+into the target branch.
+
+=== 3. *Submit the change*
+
+When all changes in the topic are submittable, you’ll see *SUBMIT WHOLE TOPIC*
+at the top of the _Change screen_. Clicking it will submit all the changes in
+"Submitted Together."
+
+image::images/cross-repository-changes-submit-topic.png[width=600]
+
+== Reverting a Submission[[reverting]]
+
+After a topic is submitted, you can revert all or one of the changes by clicking
+the *REVERT* button on any change.
+
+image::images/cross-repository-changes-revert-topic.png[width=600]
+
+This will give you the option to either revert just the change in question or
+the entire topic:
+
+image::images/cross-repository-changes-revert-topic-options.png[width=600]
+
+Reverting the entire submission creates revert commits for each change and
+automatically associates them together under the same topic. To submit
+these changes, go through the normal review process.
+
+When submitting a topic, dependent changes and their topics are submitted as
+well. The RevertSubmission creates reverts for all the changes that were
+submitted at that time. When reverting the submission described in
+<<example_submission,Example Submission>>, all 4 of those changes will get
+reverted.
+
+|===
+| NOTE: We say “reverting a submission” instead of “reverting a submitted
+ topic” because submissions are defined by submission id, not by the topic
+ string. So even though topics names could be reused, this doesn't effect
+ reverting. For example:
+
+ 1. Submission #1 uses topic A
+
+ 2. Later, Submission #2 uses topic A again
+
+ Reverting submission #2 only reverts the changes in that submission, not all
+ changes included in topic A.
+|===
+
+== Cherry-Picking a Topic
+
+You may want to cherry-pick the changes (i.e. copy the changes) of a topic to
+another branch, perhaps because you have multiple branches that all need to be
+updated with the same change (e.g. you're porting a security fix across
+branches). Gerrit provides a mechanism to create these changes.
+
+From the overflow menu (3 dot icon) in the top right of the Change Screen,
+select “Cherry pick.” In the screenshot below, we’re showing this on a
+submitted change, but this option is available if the change is pending as
+well.
+
+image::images/cross-repository-changes-cp-menu.png[width=600]
+
+Afterwards, you’ll be presented with a modal where you can “Cherry Pick entire
+topic.”
+
+image::images/cross-repository-changes-cp-modal.png[width=600]
+
+Enter the branch name that you want to target for these repositories. The
+branch must already exist on all of the repositories. After clicking
+“CHERRY PICK,” Gerrit will create new changes all targeting the entered
+branch in their respective repositories, and these new changes will all be
+associated with a new, uniquely-generated topic name.
+
+To submit the cherry-picked changes, go through the normal submission
+process.
+
+|===
+| NOTE: You cannot cherry pick two or more changes that all target the same
+ repository from the Gerrit UI at this time; you’ll get an error message saying
+ “changes cannot be of the same repository.” To accomplish this, you’d
+ need to do the cherry-pick locally.
+|===
+
+== Searching for Topics
+
+In the Gerrit search bar, you can search for changes attached to a specific
+topic using the `topic` operator e.g. `topic:MY_TOPIC_NAME`. The `intopic`
+operator works similary but supports free-text and regular expression search.
+
+You can also search for a submission using the `submissionid` operator. Topic
+submission IDs are "<id>-<topic>" where id is the change number of the change
+that triggered the submission (though this could change in the future). As a
+full example, if the topic name is my-topic and change 12345 was the one that
+triggered submission, you could find it with `submissionid:12345-my-topic`.
+
diff --git a/Documentation/images/cross-repository-changes-add-topic.png b/Documentation/images/cross-repository-changes-add-topic.png
new file mode 100644
index 0000000..fc85b8f
--- /dev/null
+++ b/Documentation/images/cross-repository-changes-add-topic.png
Binary files differ
diff --git a/Documentation/images/cross-repository-changes-cp-menu.png b/Documentation/images/cross-repository-changes-cp-menu.png
new file mode 100644
index 0000000..e9004f8
--- /dev/null
+++ b/Documentation/images/cross-repository-changes-cp-menu.png
Binary files differ
diff --git a/Documentation/images/cross-repository-changes-cp-modal.png b/Documentation/images/cross-repository-changes-cp-modal.png
new file mode 100644
index 0000000..a4790fb
--- /dev/null
+++ b/Documentation/images/cross-repository-changes-cp-modal.png
Binary files differ
diff --git a/Documentation/images/cross-repository-changes-example.png b/Documentation/images/cross-repository-changes-example.png
new file mode 100644
index 0000000..e790f71
--- /dev/null
+++ b/Documentation/images/cross-repository-changes-example.png
Binary files differ
diff --git a/Documentation/images/cross-repository-changes-revert-topic-options.png b/Documentation/images/cross-repository-changes-revert-topic-options.png
new file mode 100644
index 0000000..f2e9f1a
--- /dev/null
+++ b/Documentation/images/cross-repository-changes-revert-topic-options.png
Binary files differ
diff --git a/Documentation/images/cross-repository-changes-revert-topic.png b/Documentation/images/cross-repository-changes-revert-topic.png
new file mode 100644
index 0000000..8d87191
--- /dev/null
+++ b/Documentation/images/cross-repository-changes-revert-topic.png
Binary files differ
diff --git a/Documentation/images/cross-repository-changes-submit-topic.png b/Documentation/images/cross-repository-changes-submit-topic.png
new file mode 100644
index 0000000..7e96743
--- /dev/null
+++ b/Documentation/images/cross-repository-changes-submit-topic.png
Binary files differ
diff --git a/Documentation/images/cross-repository-changes-submitted-together.png b/Documentation/images/cross-repository-changes-submitted-together.png
new file mode 100644
index 0000000..e7ea334
--- /dev/null
+++ b/Documentation/images/cross-repository-changes-submitted-together.png
Binary files differ
diff --git a/Documentation/images/cross-repository-changes-topic.png b/Documentation/images/cross-repository-changes-topic.png
new file mode 100644
index 0000000..12d0e38
--- /dev/null
+++ b/Documentation/images/cross-repository-changes-topic.png
Binary files differ
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 8f36ecc..164039b 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -38,6 +38,7 @@
... link:user-changeid.html[Change-Id Lines]
... link:user-signedoffby.html[Signed-off-by Lines]
... link:user-change-cleanup.html[Change Cleanup]
+... link:cross-repository-changes.html[Cross Repository Changes using Topics]
== Project Management
. link:project-configuration.html[Project Configuration]
@@ -78,6 +79,7 @@
. link:note-db.html[NoteDb]
. link:config-accounts.html[Accounts on NoteDb]
. link:config-groups.html[Groups on NoteDb]
+. link:user-privacy.html[User data and privacy]
== Concepts
. link:config-labels.html[Review Labels]
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index eb2025c..0408d5d 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -514,6 +514,9 @@
$ git push origin HEAD:refs/heads/master -o topic=multi-master
----
+For more information about using topics, see the user guide:
+link:cross-repository-changes.html[Submitting Changes Across Repositories by using Topics].
+
[[hashtags]]
== Using Hashtags
diff --git a/Documentation/user-privacy.txt b/Documentation/user-privacy.txt
new file mode 100644
index 0000000..d61ee76
--- /dev/null
+++ b/Documentation/user-privacy.txt
@@ -0,0 +1,113 @@
+:linkattrs:
+= Gerrit Code Review - User Privacy
+
+== Purpose
+
+This page documents how Gerrit handles user data.
+
+|===
+| Note: Gerrit has extensive support for link:config-plugins.html[plugins]
+ which extend Gerrits functionality, and these plugins could access, export, or
+ maniuplate user data. This document only focuses on the behavior of Gerrit
+ core and its link:dev-core-plugins.html[core plugins].
+|===
+
+== Types of User Data
+
+Gerrit stores account data required for collaborating on source code changes.
+This data is described by
+link:config-accounts.html#account-data-in-user-branch[Account Data in User
+Branch] and includes link:config-accounts.html#external-ids[External IDs],
+link:config-accounts.html#preferences[User Preferences],
+link:config-accounts.html#project-watches[Project Watches] and personally
+identifiable information, including name and email address. The email
+address is required to associate Git commits with a Gerrit user account. All
+data except passwords is made accessible to other users who you are visible to,
+as detailed below.
+
+== User Visibility
+
+Gerrit has a concept of link:config-gerrit.html#accounts[account visibility]
+which determines what users a given user can see. This visibility configuration
+applies in account search, reviewer suggestion, and when accessing data through
+the link:rest-api-accounts.html#account-endpoints[Account REST endpoints]. If
+you can see a user, you have read access to most of the
+link:rest-api-accounts.html#account-info[AccountInfo] for that user, including
+name and email address. Additional information, including secondary emails, is
+included in AccountInfo if the caller has “Modify Account” permissions.
+
+Additionally, all users on a change (author, cc’d, reviewer) can see each other,
+irrespective of the account visibility settings. For example: Say you are a
+reviewer on a change where user Foo is also a reviewer. Even if by account
+visibility you could not search for Foo, you'd still see their avatar, name,
+and email now because you can see the change; this information is required to
+collaborate on a code review. If Foo wasn't on that change, you could not add
+them because reviewer suggestions would not find them due to the account
+visibility settings.
+
+By default, account visibility on a Gerrit instance is set to `ALL` which allows
+all users to be visible to other users, even anonymous (i.e. unauthenticated)
+users. Depending on your installation type, you may want to change this:
+
+* For completely company-internal Gerrit installations (no external users), the
+`ALL` default may make sense.
+
+* If you work with multiple vendors who have
+access to their own independent sets of repos, `VISIBLE_GROUP` may be more
+appropriate as you wouldn’t want vendor A to see accounts from vendor B.
+
+* For public installations, e.g. for open source projects, you may want to
+change this setting or add a notice for users when they create an account e.g.
+“Most of what you submit on this site, including your email address and name,
+will be visible to others who use this service. You may prefer to use an email
+account specifically for this purpose.” One way to do this is using
+link:config-gerrit.html[`auth.registerPageUrl`] in `gerrit.config`.
+
+== ACLs and User Visibility
+
+User suggestions for changes, when adding a reviewer or cc-ing someone, always
+respect ACLs for that change: only users who can see the change are suggested.
+The suggested users are an intersection of who you can see and who can see the
+change.
+
+Consider the following situation:
+
+* `READ` permission for Registered Users on the host
+* User visibility is set to `VISIBILE_GROUP`, so only users of the same domain can
+ see each other
+* a@foo.com creates change 123
+
+This would mean:
+
+* a@foo.com cannot add b@bar.com to the change because these users cannot see
+ each other due to the user visibility setting.
+* b@bar.com can find change 123
+ because they have READ permission and could add themselves to the change.
+* a@foo.com would then be able to see b@bar.com’s name, avatar, and email on
+ change 123
+
+The only caveat to the above are Private Changes, which are only visible to the
+owner and reviewers; reviewers can only see the change once they are added to
+the change (if ACLs allow them to be added in the first place), not before.
+
+## Right to be Forgotten Limitations
+
+As a source control system, Gerrit has limited abilities to remove personally
+identifiable information. Notably, Gerrit cannot:
+
+* Remove a user's e-mail from all existing commits
+* Remove a user's username
+
+There is also a known
+link:https://bugs.chromium.org/p/gerrit/issues/detail?id=14185[bug] where a
+user's username is stored in metadata for link:user-attention-set.html[Attention
+Set].
+
+
+## Open Source Software Limitations
+
+Gerrit is open-source software licensed under the Apache 2.0 license. 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.
\ No newline at end of file
diff --git a/java/com/google/gerrit/acceptance/ExtensionRegistry.java b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
index 5d01dcb..35f8ce6 100644
--- a/java/com/google/gerrit/acceptance/ExtensionRegistry.java
+++ b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
@@ -39,6 +39,7 @@
import com.google.gerrit.server.change.ChangeETagComputation;
import com.google.gerrit.server.config.ProjectConfigEntry;
import com.google.gerrit.server.git.ChangeMessageModifier;
+import com.google.gerrit.server.git.receive.PluginPushOption;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.git.validators.OnSubmitValidationListener;
import com.google.gerrit.server.git.validators.RefOperationValidationListener;
@@ -84,6 +85,7 @@
private final DynamicMap<CapabilityDefinition> capabilityDefinitions;
private final DynamicMap<PluginProjectPermissionDefinition> pluginProjectPermissionDefinitions;
private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
+ private final DynamicSet<PluginPushOption> pluginPushOptions;
private final DynamicSet<OnPostReview> onPostReviews;
@Inject
@@ -116,6 +118,7 @@
DynamicMap<CapabilityDefinition> capabilityDefinitions,
DynamicMap<PluginProjectPermissionDefinition> pluginProjectPermissionDefinitions,
DynamicMap<ProjectConfigEntry> pluginConfigEntries,
+ DynamicSet<PluginPushOption> pluginPushOption,
DynamicSet<OnPostReview> onPostReviews) {
this.accountIndexedListeners = accountIndexedListeners;
this.changeIndexedListeners = changeIndexedListeners;
@@ -145,6 +148,7 @@
this.capabilityDefinitions = capabilityDefinitions;
this.pluginProjectPermissionDefinitions = pluginProjectPermissionDefinitions;
this.pluginConfigEntries = pluginConfigEntries;
+ this.pluginPushOptions = pluginPushOption;
this.onPostReviews = onPostReviews;
}
@@ -274,6 +278,10 @@
return add(pluginConfigEntries, pluginConfigEntry, exportName);
}
+ public Registration add(PluginPushOption pluginPushOption) {
+ return add(pluginPushOptions, pluginPushOption);
+ }
+
public Registration add(OnPostReview onPostReview) {
return add(onPostReviews, onPostReview);
}
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index 6091091..2b49a06 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -25,6 +25,7 @@
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
@@ -546,6 +547,7 @@
cmd,
projectState.getProject(),
change.getDest().branch(),
+ ImmutableListMultimap.of(),
ctx.getRepoView().getConfig(),
ctx.getRevWalk().getObjectReader(),
commitId,
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index ef06ea1..4c1e9fb 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -20,6 +20,7 @@
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static java.util.Objects.requireNonNull;
+import com.google.common.collect.ImmutableListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
@@ -342,6 +343,7 @@
.orElseThrow(illegalState(origNotes.getProjectName()))
.getProject(),
origNotes.getChange().getDest().branch(),
+ ImmutableListMultimap.of(),
ctx.getRepoView().getConfig(),
ctx.getRevWalk().getObjectReader(),
commitId,
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 52de9d5..3da3fd3 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -130,6 +130,7 @@
import com.google.gerrit.server.git.ReceivePackInitializer;
import com.google.gerrit.server.git.TagCache;
import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.git.receive.PluginPushOption;
import com.google.gerrit.server.git.receive.ReceiveCommitsModule;
import com.google.gerrit.server.git.validators.CommentCountValidator;
import com.google.gerrit.server.git.validators.CommentCumulativeSizeValidator;
@@ -388,6 +389,7 @@
.toInstance(SuggestReviewers.configListener());
DynamicSet.setOf(binder(), ExternalIncludedIn.class);
DynamicMap.mapOf(binder(), ProjectConfigEntry.class);
+ DynamicSet.setOf(binder(), PluginPushOption.class);
DynamicSet.setOf(binder(), PatchSetWebLink.class);
DynamicSet.setOf(binder(), ParentWebLink.class);
DynamicSet.setOf(binder(), FileWebLink.class);
diff --git a/java/com/google/gerrit/server/events/CommitReceivedEvent.java b/java/com/google/gerrit/server/events/CommitReceivedEvent.java
index eb4d9ee..de355ea 100644
--- a/java/com/google/gerrit/server/events/CommitReceivedEvent.java
+++ b/java/com/google/gerrit/server/events/CommitReceivedEvent.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.events;
+import com.google.common.collect.ImmutableListMultimap;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.IdentifiedUser;
import java.io.IOException;
@@ -29,6 +30,7 @@
public ReceiveCommand command;
public Project project;
public String refName;
+ public ImmutableListMultimap<String, String> pushOptions;
public Config repoConfig;
public RevWalk revWalk;
public RevCommit commit;
@@ -42,6 +44,7 @@
ReceiveCommand command,
Project project,
String refName,
+ ImmutableListMultimap<String, String> pushOptions,
Config repoConfig,
ObjectReader reader,
ObjectId commitId,
@@ -51,6 +54,7 @@
this.command = command;
this.project = project;
this.refName = refName;
+ this.pushOptions = pushOptions;
this.repoConfig = repoConfig;
this.revWalk = new RevWalk(reader);
this.commit = revWalk.parseCommit(commitId);
diff --git a/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java b/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java
index 55261223..f680b7b 100644
--- a/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java
+++ b/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java
@@ -19,6 +19,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
@@ -109,12 +110,13 @@
ObjectReader objectReader,
ReceiveCommand cmd,
RevCommit commit,
+ ImmutableListMultimap<String, String> pushOptions,
boolean isMerged,
NoteMap rejectCommits,
@Nullable Change change)
throws IOException {
return validateCommit(
- repository, objectReader, cmd, commit, isMerged, rejectCommits, change, false);
+ repository, objectReader, cmd, commit, pushOptions, isMerged, rejectCommits, change, false);
}
/**
@@ -134,6 +136,7 @@
ObjectReader objectReader,
ReceiveCommand cmd,
RevCommit commit,
+ ImmutableListMultimap<String, String> pushOptions,
boolean isMerged,
NoteMap rejectCommits,
@Nullable Change change,
@@ -146,6 +149,7 @@
cmd,
project,
branch.branch(),
+ pushOptions,
new Config(repository.getConfig()),
objectReader,
commit,
diff --git a/java/com/google/gerrit/server/git/receive/PluginPushOption.java b/java/com/google/gerrit/server/git/receive/PluginPushOption.java
new file mode 100644
index 0000000..788df70
--- /dev/null
+++ b/java/com/google/gerrit/server/git/receive/PluginPushOption.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.receive;
+
+/**
+ * Push option that can be specified on push.
+ *
+ * <p>On push the option has to be specified as {@code -o <pluginName>~<name>=<value>}, or if a
+ * value is not required as {@code -o <pluginName>~<name>}.
+ */
+public interface PluginPushOption {
+ /** The name of the push option. */
+ public String getName();
+
+ /** The description of the push option. */
+ public String getDescription();
+}
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 4c90ef9..4600f6d 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -17,6 +17,7 @@
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.flogger.LazyArgs.lazy;
import static com.google.gerrit.entities.RefNames.REFS_CHANGES;
@@ -48,6 +49,7 @@
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
@@ -211,6 +213,7 @@
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
@@ -326,6 +329,7 @@
private final CreateGroupPermissionSyncer createGroupPermissionSyncer;
private final CreateRefControl createRefControl;
private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
+ private final DynamicSet<PluginPushOption> pluginPushOptions;
private final PluginSetContext<ReceivePackInitializer> initializers;
private final MergedByPushOp.Factory mergedByPushOpFactory;
private final PatchSetInfoFactory patchSetInfoFactory;
@@ -408,6 +412,7 @@
CreateGroupPermissionSyncer createGroupPermissionSyncer,
CreateRefControl createRefControl,
DynamicMap<ProjectConfigEntry> pluginConfigEntries,
+ DynamicSet<PluginPushOption> pluginPushOptions,
PluginSetContext<ReceivePackInitializer> initializers,
PluginSetContext<CommentValidator> commentValidators,
MergedByPushOp.Factory mergedByPushOpFactory,
@@ -467,6 +472,7 @@
this.patchSetInfoFactory = patchSetInfoFactory;
this.permissionBackend = permissionBackend;
this.pluginConfigEntries = pluginConfigEntries;
+ this.pluginPushOptions = pluginPushOptions;
this.projectCache = projectCache;
this.psUtil = psUtil;
this.performanceLoggers = performanceLoggers;
@@ -1788,8 +1794,13 @@
String ref;
magicBranch.cmdLineParser = optionParserFactory.create(magicBranch);
+ // Filter out plugin push options, as the parser would reject them as unknown.
+ ImmutableListMultimap<String, String> pushOptionsToParse =
+ pushOptions.entries().stream()
+ .filter(e -> !isPluginPushOption(e.getKey()))
+ .collect(toImmutableListMultimap(e -> e.getKey(), e -> e.getValue()));
try {
- ref = magicBranch.parse(pushOptions);
+ ref = magicBranch.parse(pushOptionsToParse);
} catch (CmdLineException e) {
if (!magicBranch.cmdLineParser.wasHelpRequestedByOption()) {
logger.atFine().log("Invalid branch syntax");
@@ -1808,6 +1819,20 @@
StringWriter w = new StringWriter();
w.write("\nHelp for refs/for/branch:\n\n");
magicBranch.cmdLineParser.printUsage(w, null);
+
+ String pluginPushOptionsHelp =
+ StreamSupport.stream(pluginPushOptions.entries().spliterator(), /* parallel= */ false)
+ .map(
+ e ->
+ String.format(
+ "-o %s~%s: %s",
+ e.getPluginName(), e.get().getName(), e.get().getDescription()))
+ .sorted()
+ .collect(joining("\n"));
+ if (!pluginPushOptionsHelp.isEmpty()) {
+ w.write("\nPlugin push options:\n" + pluginPushOptionsHelp);
+ }
+
addMessage(w.toString());
reject(cmd, "see help");
return;
@@ -1972,6 +1997,11 @@
}
}
+ private boolean isPluginPushOption(String pushOptionName) {
+ return StreamSupport.stream(pluginPushOptions.entries().spliterator(), /* parallel= */ false)
+ .anyMatch(e -> pushOptionName.equals(e.getPluginName() + "~" + e.get().getName()));
+ }
+
// Validate that the new commits are connected with the target
// branch. If they aren't, we want to abort. We do this check by
// looking to see if we can compute a merge base between the new
@@ -2219,6 +2249,7 @@
receivePack.getRevWalk().getObjectReader(),
magicBranch.cmd,
c,
+ ImmutableListMultimap.copyOf(pushOptions),
magicBranch.merged,
rejectCommits,
null);
@@ -3231,7 +3262,15 @@
BranchCommitValidator.Result validationResult =
validator.validateCommit(
- repo, walk.getObjectReader(), cmd, c, false, rejectCommits, null, skipValidation);
+ repo,
+ walk.getObjectReader(),
+ cmd,
+ c,
+ ImmutableListMultimap.copyOf(pushOptions),
+ false,
+ rejectCommits,
+ null,
+ skipValidation);
messages.addAll(validationResult.messages());
if (!validationResult.isValid()) {
break;
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 845c461..eac0f1b 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -62,6 +62,7 @@
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Address;
@@ -100,6 +101,7 @@
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.receive.NoteDbPushOption;
+import com.google.gerrit.server.git.receive.PluginPushOption;
import com.google.gerrit.server.git.receive.ReceiveConstants;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
@@ -2364,6 +2366,8 @@
private final AtomicInteger count = new AtomicInteger();
private final boolean validateAll;
+ @Nullable private CommitReceivedEvent receivedEvent;
+
TestValidator(boolean validateAll) {
this.validateAll = validateAll;
}
@@ -2373,7 +2377,8 @@
}
@Override
- public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent) {
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receivedEvent) {
+ this.receivedEvent = receivedEvent;
count.incrementAndGet();
return Collections.emptyList();
}
@@ -2386,6 +2391,31 @@
public int count() {
return count.get();
}
+
+ @Nullable
+ public CommitReceivedEvent getReceivedEvent() {
+ return receivedEvent;
+ }
+ }
+
+ private static class TestPluginPushOption implements PluginPushOption {
+ private final String name;
+ private final String description;
+
+ TestPluginPushOption(String name, String description) {
+ this.name = name;
+ this.description = description;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
}
private static class TopicValidator implements TopicEditedListener {
@@ -2449,6 +2479,38 @@
}
@Test
+ public void pushOptionsArePassedToCommitValidationListener() throws Exception {
+ TestValidator validator = new TestValidator();
+ PluginPushOption fooOption = new TestPluginPushOption("foo", "some description");
+ PluginPushOption barOption = new TestPluginPushOption("bar", "other description");
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(validator).add(fooOption).add(barOption)) {
+ PushOneCommit push =
+ pushFactory.create(admin.newIdent(), testRepo, "change2", "b.txt", "content");
+ push.setPushOptions(ImmutableList.of("trace=123", "gerrit~foo", "gerrit~bar=456"));
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertOkStatus();
+ assertThat(validator.getReceivedEvent().pushOptions)
+ .containsExactly("trace", "123", "gerrit~foo", "", "gerrit~bar", "456");
+ }
+ }
+
+ @Test
+ public void pluginPushOptionsHelp() throws Exception {
+ PluginPushOption fooOption = new TestPluginPushOption("foo", "some description");
+ PluginPushOption barOption = new TestPluginPushOption("bar", "other description");
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(fooOption).add(barOption)) {
+ PushOneCommit push =
+ pushFactory.create(admin.newIdent(), testRepo, "change2", "b.txt", "content");
+ push.setPushOptions(ImmutableList.of("help"));
+ PushOneCommit.Result r = push.to("refs/for/master");
+ r.assertErrorStatus("see help");
+ r.assertMessage("-o gerrit~bar: other description\n-o gerrit~foo: some description\n");
+ }
+ }
+
+ @Test
public void pushNoteDbRef() throws Exception {
String ref = "refs/changes/34/1234/meta";
RevCommit c = testRepo.commit().message("Junk NoteDb commit").create();
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts
index d1c780d..678fa5c 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.ts
@@ -51,8 +51,7 @@
float: right;
}
.title {
- /* 225 is length of gr-limited-text with limit 25 */
- width: 225px;
+ min-width: 10em;
padding: var(--spacing-s) var(--spacing-m) 0
var(--requirements-horizontal-padding);
}
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 7fb09df..e0e2e46 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
@@ -21,7 +21,6 @@
import '../../shared/gr-button/gr-button';
import '../../shared/gr-formatted-text/gr-formatted-text';
import '../../shared/gr-overlay/gr-overlay';
-import '../../shared/gr-storage/gr-storage';
import '../../shared/gr-account-list/gr-account-list';
import '../gr-label-scores/gr-label-scores';
import '../gr-thread-list/gr-thread-list';
@@ -98,7 +97,6 @@
import {GrTextarea} from '../../shared/gr-textarea/gr-textarea';
import {GrAccountChip} from '../../shared/gr-account-chip/gr-account-chip';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
-import {GrStorage, StorageLocation} from '../../shared/gr-storage/gr-storage';
import {isAttentionSetEnabled} from '../../../utils/attention-set-util';
import {
CODE_REVIEW,
@@ -114,6 +112,7 @@
} from '../../../utils/event-util';
import {ErrorCallback} from '../../../api/rest';
import {debounce, DelayedTask} from '../../../utils/async-util';
+import {StorageLocation} from '../../../services/storage/gr-storage';
const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
@@ -374,7 +373,7 @@
private readonly restApiService = appContext.restApiService;
- private readonly storage = new GrStorage();
+ private readonly storage = appContext.storageService;
private readonly jsAPI = appContext.jsApiService;
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
index d0d40ca..c1e2564 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
@@ -18,7 +18,7 @@
import '../../../test/common-test-setup-karma.js';
import {IronOverlayManager} from '@polymer/iron-overlay-behavior/iron-overlay-manager.js';
import './gr-reply-dialog.js';
-import {mockPromise} from '../../../test/test-utils.js';
+import {mockPromise, stubStorage} from '../../../test/test-utils.js';
import {SpecialFilePath} from '../../../constants/constants.js';
import {appContext} from '../../../services/app-context.js';
import {addListenerForTest} from '../../../test/test-utils.js';
@@ -113,9 +113,9 @@
],
};
- getDraftCommentStub = sinon.stub(element.storage, 'getDraftComment');
- setDraftCommentStub = sinon.stub(element.storage, 'setDraftComment');
- eraseDraftCommentStub = sinon.stub(element.storage, 'eraseDraftComment');
+ getDraftCommentStub = stubStorage('getDraftComment');
+ setDraftCommentStub = stubStorage('setDraftComment');
+ eraseDraftCommentStub = stubStorage('eraseDraftComment');
// sinon.stub(patchSetUtilMockProxy, 'fetchChangeUpdates')
// .returns(Promise.resolve({isLatest: true}));
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index 7c4e8d6..fb8c2e0 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -18,7 +18,6 @@
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-editable-label/gr-editable-label';
-import '../../shared/gr-storage/gr-storage';
import '../gr-default-editor/gr-default-editor';
import '../../../styles/shared-styles';
import {PolymerElement} from '@polymer/polymer/polymer-element';
@@ -38,7 +37,6 @@
NumericChangeId,
EditPatchSetNum,
} from '../../../types/common';
-import {GrStorage} from '../../shared/gr-storage/gr-storage';
import {HttpMethod, NotifyType} from '../../../constants/constants';
import {fireAlert, fireTitleChange} from '../../../utils/event-util';
import {appContext} from '../../../services/app-context';
@@ -117,7 +115,7 @@
private readonly restApiService = appContext.restApiService;
- private readonly storage = new GrStorage();
+ private readonly storage = appContext.storageService;
private storeTask?: DelayedTask;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.js
deleted file mode 100644
index f05ed21..0000000
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.js
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
- * @license
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import './gr-change-star.js';
-
-const basicFixture = fixtureFromElement('gr-change-star');
-
-suite('gr-change-star tests', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- element.change = {
- _number: 2,
- starred: true,
- };
- });
-
- test('star visibility states', () => {
- element.set('change.starred', true);
- let icon = element.shadowRoot
- .querySelector('iron-icon');
- assert.isTrue(icon.classList.contains('active'));
- assert.equal(icon.icon, 'gr-icons:star');
-
- element.set('change.starred', false);
- icon = element.shadowRoot
- .querySelector('iron-icon');
- assert.isFalse(icon.classList.contains('active'));
- assert.equal(icon.icon, 'gr-icons:star-border');
- });
-
- test('starring', async () => {
- let resolve;
- const promise = new Promise(r => resolve = r);
- element.addEventListener('toggle-star', resolve);
- element.set('change.starred', false);
- MockInteractions.tap(element.shadowRoot
- .querySelector('button'));
-
- await promise;
- assert.equal(element.change.starred, true);
- });
-
- test('unstarring', async () => {
- let resolve;
- const promise = new Promise(r => resolve = r);
- element.addEventListener('toggle-star', resolve);
- element.set('change.starred', true);
- MockInteractions.tap(element.shadowRoot
- .querySelector('button'));
-
- await promise;
- assert.equal(element.change.starred, false);
- });
-});
-
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts
new file mode 100644
index 0000000..8f411ae
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts
@@ -0,0 +1,71 @@
+/**
+ * @license
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {IronIconElement} from '@polymer/iron-icon';
+import '../../../test/common-test-setup-karma';
+import {queryAndAssert} from '../../../test/test-utils';
+import {GrChangeStar} from './gr-change-star';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
+import {createChange} from '../../../test/test-data-generators';
+
+const basicFixture = fixtureFromElement('gr-change-star');
+
+suite('gr-change-star tests', () => {
+ let element: GrChangeStar;
+
+ setup(() => {
+ element = basicFixture.instantiate();
+ element.change = {
+ ...createChange(),
+ starred: true,
+ };
+ });
+
+ test('star visibility states', async () => {
+ element.set('change.starred', true);
+ await flush();
+ let icon = queryAndAssert<IronIconElement>(element, 'iron-icon');
+ assert.isTrue(icon.classList.contains('active'));
+ assert.equal(icon.icon, 'gr-icons:star');
+
+ element.set('change.starred', false);
+ await flush();
+ icon = queryAndAssert<IronIconElement>(element, 'iron-icon');
+ assert.isFalse(icon.classList.contains('active'));
+ assert.equal(icon.icon, 'gr-icons:star-border');
+ });
+
+ test('starring', async () => {
+ element.set('change.starred', false);
+ await flush();
+ assert.equal(element.change!.starred, false);
+
+ MockInteractions.tap(queryAndAssert(element, 'button'));
+ await flush();
+ assert.equal(element.change!.starred, true);
+ });
+
+ test('unstarring', async () => {
+ element.set('change.starred', true);
+ await flush();
+ assert.equal(element.change!.starred, true);
+
+ MockInteractions.tap(queryAndAssert(element, 'button'));
+ await flush();
+ assert.equal(element.change!.starred, false);
+ });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index a57bdfa..7e67fb8 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -15,7 +15,6 @@
* limitations under the License.
*/
import '../../../styles/shared-styles';
-import '../gr-storage/gr-storage';
import '../gr-comment/gr-comment';
import '../../diff/gr-diff/gr-diff';
import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
@@ -51,7 +50,6 @@
} from '../../../types/common';
import {GrComment} from '../gr-comment/gr-comment';
import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
-import {GrStorage, StorageLocation} from '../gr-storage/gr-storage';
import {CustomKeyboardEvent} from '../../../types/events';
import {LineNumber, FILE} from '../../diff/gr-diff/gr-diff-line';
import {GrButton} from '../gr-button/gr-button';
@@ -61,6 +59,7 @@
import {check, assertIsDefined} from '../../../utils/common-util';
import {waitForEventOnce} from '../../../utils/event-util';
import {GrSyntaxLayer} from '../../diff/gr-syntax-layer/gr-syntax-layer';
+import {StorageLocation} from '../../../services/storage/gr-storage';
const UNRESOLVED_EXPAND_COUNT = 5;
const NEWLINE_PATTERN = /\n/g;
@@ -208,7 +207,7 @@
flagsService = appContext.flagsService;
- readonly storage = new GrStorage();
+ readonly storage = appContext.storageService;
private readonly syntaxLayer = new GrSyntaxLayer();
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
index 8b15203..35b0d8c 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
@@ -44,8 +44,8 @@
tap,
pressAndReleaseKeyOn,
} from '@polymer/iron-test-helpers/mock-interactions';
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-import {stubRestApi} from '../../../test/test-utils';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import {stubRestApi, stubStorage} from '../../../test/test-utils';
const basicFixture = fixtureFromElement('gr-comment-thread');
@@ -652,7 +652,7 @@
__draft: true,
},
];
- const storageStub = sinon.stub(element.storage, 'setDraftComment');
+ const storageStub = stubStorage('setDraftComment');
flush();
const draftEl = element.root?.querySelectorAll('gr-comment')[1];
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 7fb85a4..b7f1bcc 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -24,7 +24,6 @@
import '../gr-formatted-text/gr-formatted-text';
import '../gr-icons/gr-icons';
import '../gr-overlay/gr-overlay';
-import '../gr-storage/gr-storage';
import '../gr-textarea/gr-textarea';
import '../gr-tooltip-content/gr-tooltip-content';
import '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
@@ -38,7 +37,6 @@
import {customElement, observe, property} from '@polymer/decorators';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
import {GrTextarea} from '../gr-textarea/gr-textarea';
-import {GrStorage, StorageLocation} from '../gr-storage/gr-storage';
import {GrOverlay} from '../gr-overlay/gr-overlay';
import {
AccountDetailInfo,
@@ -62,6 +60,7 @@
import {pluralize} from '../../../utils/string-util';
import {assertIsDefined} from '../../../utils/common-util';
import {debounce, DelayedTask} from '../../../utils/async-util';
+import {StorageLocation} from '../../../services/storage/gr-storage';
const STORAGE_DEBOUNCE_INTERVAL = 400;
const TOAST_DEBOUNCE_INTERVAL = 200;
@@ -269,7 +268,7 @@
private readonly restApiService = appContext.restApiService;
- private readonly storage = new GrStorage();
+ private readonly storage = appContext.storageService;
reporting = appContext.reportingService;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
index b5205a6..be647ab 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
@@ -20,7 +20,7 @@
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
import {__testOnly_UNSAVED_MESSAGE} from './gr-comment.js';
import {SpecialFilePath, Side} from '../../../constants/constants.js';
-import {stubRestApi} from '../../../test/test-utils.js';
+import {stubRestApi, stubStorage} from '../../../test/test-utils.js';
const basicFixture = fixtureFromElement('gr-comment');
@@ -115,7 +115,7 @@
});
test('message is not retrieved from storage when other edits', done => {
- const storageStub = sinon.stub(element.storage, 'getDraftComment');
+ const storageStub = stubStorage('getDraftComment');
const loadSpy = sinon.spy(element, '_loadLocalDraft');
element.changeNum = 1;
@@ -135,7 +135,7 @@
});
test('message is retrieved from storage when no other edits', done => {
- const storageStub = sinon.stub(element.storage, 'getDraftComment');
+ const storageStub = stubStorage('getDraftComment');
const loadSpy = sinon.spy(element, '_loadLocalDraft');
element.changeNum = 1;
@@ -1022,8 +1022,8 @@
test('cancelling an unsaved draft discards, persists in storage', () => {
const discardSpy = sinon.spy(element, '_fireDiscard');
- const storeStub = sinon.stub(element.storage, 'setDraftComment');
- const eraseStub = sinon.stub(element.storage, 'eraseDraftComment');
+ const storeStub = stubStorage('setDraftComment');
+ const eraseStub = stubStorage('eraseDraftComment');
element._messageText = 'test text';
flush();
element.storeTask.flush();
@@ -1038,7 +1038,7 @@
test('cancelling edit on a saved draft does not store', () => {
element.comment.id = 'foo';
const discardSpy = sinon.spy(element, '_fireDiscard');
- const storeStub = sinon.stub(element.storage, 'setDraftComment');
+ const storeStub = stubStorage('setDraftComment');
element._messageText = 'test text';
flush();
if (element.storeTask) element.storeTask.flush();
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
index 12f07ba..096697f 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
@@ -16,9 +16,7 @@
*/
import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
import '../../../styles/shared-styles';
-import '../gr-storage/gr-storage';
import '../gr-button/gr-button';
-import {GrStorage} from '../gr-storage/gr-storage';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {customElement, property} from '@polymer/decorators';
import {htmlTemplate} from './gr-editable-content_html';
@@ -111,7 +109,7 @@
@property({type: Boolean})
_isNewChangeSummaryUiEnabled = false;
- private readonly storage = new GrStorage();
+ private readonly storage = appContext.storageService;
private readonly flagsService = appContext.flagsService;
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index 0369ccf..479ab88 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -23,6 +23,7 @@
import {ChangeService} from './change/change-service';
import {ChecksService} from './checks/checks-service';
import {GrJsApiInterface} from '../elements/shared/gr-js-api-interface/gr-js-api-interface-element';
+import {GrStorageService} from './storage/gr-storage_impl';
type ServiceName = keyof AppContext;
type ServiceCreator<T> = () => T;
@@ -73,5 +74,6 @@
changeService: () => new ChangeService(),
checksService: () => new ChecksService(),
jsApiService: () => new GrJsApiInterface(),
+ storageService: () => new GrStorageService(),
});
}
diff --git a/polygerrit-ui/app/services/app-context.ts b/polygerrit-ui/app/services/app-context.ts
index 1f618fd..cf186f0 100644
--- a/polygerrit-ui/app/services/app-context.ts
+++ b/polygerrit-ui/app/services/app-context.ts
@@ -22,6 +22,7 @@
import {ChangeService} from './change/change-service';
import {ChecksService} from './checks/checks-service';
import {JsApiService} from '../elements/shared/gr-js-api-interface/gr-js-api-types';
+import {StorageService} from './storage/gr-storage';
export interface AppContext {
flagsService: FlagsService;
@@ -32,6 +33,7 @@
changeService: ChangeService;
checksService: ChecksService;
jsApiService: JsApiService;
+ storageService: StorageService;
}
/**
diff --git a/polygerrit-ui/app/services/storage/gr-storage.ts b/polygerrit-ui/app/services/storage/gr-storage.ts
new file mode 100644
index 0000000..08a3387
--- /dev/null
+++ b/polygerrit-ui/app/services/storage/gr-storage.ts
@@ -0,0 +1,49 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {CommentRange, PatchSetNum} from '../../types/common';
+
+export interface StorageLocation {
+ changeNum: number;
+ patchNum: PatchSetNum | '@change';
+ path?: string;
+ line?: number;
+ range?: CommentRange;
+}
+
+export interface StorageObject {
+ message?: string;
+ updated: number;
+}
+
+export interface StorageService {
+ getDraftComment(location: StorageLocation): StorageObject | null;
+
+ setDraftComment(location: StorageLocation, message: string): void;
+
+ eraseDraftComment(location: StorageLocation): void;
+
+ getEditableContentItem(key: string): StorageObject | null;
+
+ setEditableContentItem(key: string, message: string): void;
+
+ getRespectfulTipVisibility(): StorageObject | null;
+
+ setRespectfulTipVisibility(delayDays?: number): void;
+
+ eraseEditableContentItem(key: string): void;
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.ts b/polygerrit-ui/app/services/storage/gr-storage_impl.ts
similarity index 72%
rename from polygerrit-ui/app/elements/shared/gr-storage/gr-storage.ts
rename to polygerrit-ui/app/services/storage/gr-storage_impl.ts
index a86d8f2..0c0d151 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.ts
+++ b/polygerrit-ui/app/services/storage/gr-storage_impl.ts
@@ -14,22 +14,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {CommentRange, PatchSetNum} from '../../../types/common';
-export interface StorageLocation {
- changeNum: number;
- patchNum: PatchSetNum | '@change';
- path?: string;
- line?: number;
- range?: CommentRange;
-}
+import {StorageLocation, StorageObject, StorageService} from './gr-storage';
-export interface StorageObject {
- message?: string;
- updated: number;
-}
-
-const DURATION_DAY = 24 * 60 * 60 * 1000;
+export const DURATION_DAY = 24 * 60 * 60 * 1000;
// Clean up old entries no more frequently than one day.
const CLEANUP_THROTTLE_INTERVAL = DURATION_DAY;
@@ -39,7 +27,7 @@
CLEANUP_PREFIXES_MAX_AGE_MAP.set('draft', DURATION_DAY);
CLEANUP_PREFIXES_MAX_AGE_MAP.set('editablecontent', DURATION_DAY);
-export class GrStorage {
+export class GrStorageService implements StorageService {
private lastCleanup = 0;
private readonly storage = window.localStorage;
@@ -47,49 +35,49 @@
private exceededQuota = false;
getDraftComment(location: StorageLocation): StorageObject | null {
- this._cleanupItems();
- return this._getObject(this._getDraftKey(location));
+ this.cleanupItems();
+ return this.getObject(this.getDraftKey(location));
}
setDraftComment(location: StorageLocation, message: string) {
- const key = this._getDraftKey(location);
- this._setObject(key, {message, updated: Date.now()});
+ const key = this.getDraftKey(location);
+ this.setObject(key, {message, updated: Date.now()});
}
eraseDraftComment(location: StorageLocation) {
- const key = this._getDraftKey(location);
+ const key = this.getDraftKey(location);
this.storage.removeItem(key);
}
getEditableContentItem(key: string): StorageObject | null {
- this._cleanupItems();
- return this._getObject(this._getEditableContentKey(key));
+ this.cleanupItems();
+ return this.getObject(this.getEditableContentKey(key));
}
setEditableContentItem(key: string, message: string) {
- this._setObject(this._getEditableContentKey(key), {
+ this.setObject(this.getEditableContentKey(key), {
message,
updated: Date.now(),
});
}
getRespectfulTipVisibility(): StorageObject | null {
- this._cleanupItems();
- return this._getObject('respectfultip:visibility');
+ this.cleanupItems();
+ return this.getObject('respectfultip:visibility');
}
setRespectfulTipVisibility(delayDays = 0) {
- this._cleanupItems();
- this._setObject('respectfultip:visibility', {
+ this.cleanupItems();
+ this.setObject('respectfultip:visibility', {
updated: Date.now() + delayDays * DURATION_DAY,
});
}
eraseEditableContentItem(key: string) {
- this.storage.removeItem(this._getEditableContentKey(key));
+ this.storage.removeItem(this.getEditableContentKey(key));
}
- _getDraftKey(location: StorageLocation): string {
+ private getDraftKey(location: StorageLocation): string {
const range = location.range
? `${location.range.start_line}-${location.range.start_character}` +
`-${location.range.end_character}-${location.range.end_line}`
@@ -107,11 +95,11 @@
return key;
}
- _getEditableContentKey(key: string): string {
+ private getEditableContentKey(key: string): string {
return `editablecontent:${key}`;
}
- _cleanupItems() {
+ private cleanupItems() {
// Throttle cleanup to the throttle interval.
if (
this.lastCleanup &&
@@ -125,7 +113,7 @@
const entries = CLEANUP_PREFIXES_MAX_AGE_MAP.entries();
for (const [prefix, expiration] of entries) {
if (key.startsWith(prefix)) {
- const item = this._getObject(key);
+ const item = this.getObject(key);
if (!item || Date.now() - item.updated > expiration) {
this.storage.removeItem(key);
}
@@ -134,7 +122,7 @@
});
}
- _getObject(key: string): StorageObject | null {
+ private getObject(key: string): StorageObject | null {
const serial = this.storage.getItem(key);
if (!serial) {
return null;
@@ -142,7 +130,7 @@
return JSON.parse(serial) as StorageObject;
}
- _setObject(key: string, obj: StorageObject) {
+ private setObject(key: string, obj: StorageObject) {
if (this.exceededQuota) {
return;
}
diff --git a/polygerrit-ui/app/services/storage/gr-storage_mock.ts b/polygerrit-ui/app/services/storage/gr-storage_mock.ts
new file mode 100644
index 0000000..02215a8
--- /dev/null
+++ b/polygerrit-ui/app/services/storage/gr-storage_mock.ts
@@ -0,0 +1,91 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {StorageLocation, StorageObject, StorageService} from './gr-storage';
+import {DURATION_DAY} from './gr-storage_impl';
+
+const storage = new Map();
+
+const getDraftKey = (location: StorageLocation): string => {
+ const range = location.range
+ ? `${location.range.start_line}-${location.range.start_character}` +
+ `-${location.range.end_character}-${location.range.end_line}`
+ : null;
+ let key = [
+ 'draft',
+ location.changeNum,
+ location.patchNum,
+ location.path,
+ location.line || '',
+ ].join(':');
+ if (range) {
+ key = key + ':' + range;
+ }
+ return key;
+};
+
+const getEditableContentKey = (key: string): string => {
+ return `editablecontent:${key}`;
+};
+
+export function cleanUpStorage() {
+ storage.clear();
+}
+
+export const grStorageMock: StorageService = {
+ getDraftComment(location: StorageLocation): StorageObject | null {
+ return storage.get(getDraftKey(location));
+ },
+
+ setDraftComment(location: StorageLocation, message: string) {
+ const key = getDraftKey(location);
+ storage.set(key, {message, updated: Date.now()});
+ },
+
+ eraseDraftComment(location: StorageLocation) {
+ const key = getDraftKey(location);
+ storage.delete(key);
+ },
+
+ getEditableContentItem(key: string): StorageObject | null {
+ return storage.get(getEditableContentKey(key));
+ },
+
+ setEditableContentItem(key: string, message: string): void {
+ storage.set(
+ getEditableContentKey(key),
+ JSON.stringify({
+ message,
+ updated: Date.now(),
+ })
+ );
+ },
+
+ getRespectfulTipVisibility(): StorageObject | null {
+ return storage.get('respectfultip:visibility');
+ },
+
+ setRespectfulTipVisibility(delayDays = 0): void {
+ storage.set('respectfultip:visibility', {
+ updated: Date.now() + delayDays * DURATION_DAY,
+ });
+ },
+
+ eraseEditableContentItem(key: string): void {
+ storage.delete(getEditableContentKey(key));
+ },
+};
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.js b/polygerrit-ui/app/services/storage/gr-storage_test.js
similarity index 87%
rename from polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.js
rename to polygerrit-ui/app/services/storage/gr-storage_test.js
index 64d3750..6cbfacf 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.js
+++ b/polygerrit-ui/app/services/storage/gr-storage_test.js
@@ -15,8 +15,8 @@
* limitations under the License.
*/
-import '../../../test/common-test-setup-karma.js';
-import {GrStorage} from './gr-storage.js';
+import '../../test/common-test-setup-karma.js';
+import {GrStorageService} from './gr-storage_impl.js';
suite('gr-storage tests', () => {
let grStorage;
@@ -34,7 +34,7 @@
}
setup(() => {
- grStorage = new GrStorage();
+ grStorage = new GrStorageService();
grStorage.storage = mockStorage();
});
@@ -51,7 +51,7 @@
};
// The key is in the expected format.
- const key = grStorage._getDraftKey(location);
+ const key = grStorage.getDraftKey(location);
assert.equal(key, ['draft', changeNum, patchNum, path, line].join(':'));
// There should be no draft initially.
@@ -82,12 +82,12 @@
line,
};
- const key = grStorage._getDraftKey(location);
+ const key = grStorage.getDraftKey(location);
// Make sure that the call to cleanup doesn't get throttled.
grStorage.lastCleanup = 0;
- const cleanupSpy = sinon.spy(grStorage, '_cleanupItems');
+ const cleanupSpy = sinon.spy(grStorage, 'cleanupItems');
// Create a message with a timestamp that is a second behind the max age.
grStorage.storage.setItem(key, JSON.stringify({
@@ -103,7 +103,7 @@
assert.isNotOk(grStorage.storage.getItem(key));
});
- test('_getDraftKey', () => {
+ test('getDraftKey', () => {
const changeNum = 1234;
const patchNum = 5;
const path = 'my_source_file.js';
@@ -115,7 +115,7 @@
line,
};
let expectedResult = 'draft:1234:5:my_source_file.js:123';
- assert.equal(grStorage._getDraftKey(location), expectedResult);
+ assert.equal(grStorage.getDraftKey(location), expectedResult);
location.range = {
start_character: 1,
start_line: 1,
@@ -123,7 +123,7 @@
end_line: 2,
};
expectedResult = 'draft:1234:5:my_source_file.js:123:1-1-1-2';
- assert.equal(grStorage._getDraftKey(location), expectedResult);
+ assert.equal(grStorage.getDraftKey(location), expectedResult);
});
test('exceeded quota disables storage', () => {
@@ -140,16 +140,16 @@
path,
line,
};
- const key = grStorage._getDraftKey(location);
+ const key = grStorage.getDraftKey(location);
grStorage.setDraftComment(location, 'my comment');
assert.isTrue(grStorage.exceededQuota);
assert.isNotOk(grStorage.storage.getItem(key));
});
test('editable content items', () => {
- const cleanupStub = sinon.stub(grStorage, '_cleanupItems');
+ const cleanupStub = sinon.stub(grStorage, 'cleanupItems');
const key = 'testKey';
- const computedKey = grStorage._getEditableContentKey(key);
+ const computedKey = grStorage.getEditableContentKey(key);
// Key correctly computed.
assert.equal(computedKey, 'editablecontent:testKey');
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index 016c1e4..641e4b3 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -43,6 +43,7 @@
installPolymerResin,
} from '../scripts/polymer-resin-install';
import {_testOnly_allTasks} from '../utils/async-util';
+import {cleanUpStorage} from '../services/storage/gr-storage_mock';
declare global {
interface Window {
@@ -200,6 +201,7 @@
checkGlobalSpace();
removeIronOverlayBackdropStyleEl();
cancelAllTasks();
+ cleanUpStorage();
const testTeardownTimestampMs = new Date().getTime();
const elapsedMs = testTeardownTimestampMs - testSetupTimestampMs;
if (elapsedMs > 1000) {
diff --git a/polygerrit-ui/app/test/test-app-context-init.ts b/polygerrit-ui/app/test/test-app-context-init.ts
index 57afd8a..d74a9c1 100644
--- a/polygerrit-ui/app/test/test-app-context-init.ts
+++ b/polygerrit-ui/app/test/test-app-context-init.ts
@@ -20,6 +20,7 @@
import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock';
import {AppContext, appContext} from '../services/app-context';
import {grRestApiMock} from './mocks/gr-rest-api_mock';
+import {grStorageMock} from '../services/storage/gr-storage_mock';
export function _testOnlyInitAppContext() {
initAppContext();
@@ -36,4 +37,5 @@
}
setMock('reportingService', grReportingMock);
setMock('restApiService', grRestApiMock);
+ setMock('storageService', grStorageMock);
}
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 50f465f..26b99ac 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -24,6 +24,7 @@
import {appContext} from '../services/app-context';
import {RestApiService} from '../services/gr-rest-api/gr-rest-api';
import {SinonSpy} from 'sinon/pkg/sinon-esm';
+import {StorageService} from '../services/storage/gr-storage';
export interface MockPromise extends Promise<unknown> {
resolve: (value?: unknown) => void;
@@ -165,6 +166,10 @@
return sinon.spy(appContext.restApiService, method);
}
+export function stubStorage<K extends keyof StorageService>(method: K) {
+ return sinon.stub(appContext.storageService, method);
+}
+
export type SinonSpyMember<F extends (...args: any) => any> = SinonSpy<
Parameters<F>,
ReturnType<F>
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index 9c3c933..3f5ef20 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -1,5 +1,4 @@
load("//tools/bzl:maven_jar.bzl", "maven_jar")
-load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file")
GUAVA_VERSION = "30.1-jre"