Merge changes I8c01f550,Ic53dfcf7,I20118b57
* changes:
SubscriptionGraph: Make the factory an interface
SubmoduleOp: Create CircularPathFinder class
SubscriptionGraph: Container for subscription relation
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 1bf51a1..1f4fd9c 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2242,6 +2242,25 @@
+
By default false.
+[[gerrit.xframeOption]]gerrit.xframeOption::
++
+Add link:https://tools.ietf.org/html/rfc7034[`X-Frame-Options`] header to all HTTP
+responses. The `X-Frame-Options` HTTP response header can be used to indicate
+whether or not a browser should be allowed to render a page in a
+`<frame>`, `<iframe>`, `<embed>` or `<object>`.
++
+Available values:
++
+1. ALLOW - The page can be displayed in a frame.
+2. SAMEORIGIN - The page can only be displayed in a frame on the same origin as the page itself.
++
+If link:#gerrit.canLoadInIFrame is set to false this option is ignored and the
+`X-Frame-Options` header is always set to `DENY`.
+Setting this option to `ALLOW` will cause the `X-Frame-Options` header to be omitted
+the the page can be displayed in a frame.
++
+By default SAMEORIGIN.
+
[[gerrit.cdnPath]]gerrit.cdnPath::
+
Path prefix for PolyGerrit's static resources if using a CDN.
@@ -2265,6 +2284,36 @@
+
Defaults to the full hostname of the Gerrit server.
+[[gerrit.experimentalRollingUpgrade]]gerrit.experimentalRollingUpgrade::
++
+Enable Gerrit rolling upgrade to the next version.
+For example if Gerrit v3.1 is version N (All-Projects:refs/meta/version=181)
+then its next version N+1 is v3.2 (All-Projects:refs/meta/version=183).
+Allow Gerrit to start even if the underlying schema version has been bumped to
+the next Gerrit version.
++
+Set to true if Gerrit is installed in
+[high-availability configuration](https://gerrit.googlesource.com/plugins/high-availability/+/refs/heads/master/README.md)
+during the rolling upgrade to the next version.
++
+By default false.
++
+The rolling upgrade process, at high level, assumes that Gerrit is installed
+on two or more nodes sharing the repositories over NFS. The upgrade is composed
+of the following steps:
++
+1. Set gerrit.experimentalRollingUpgrade to true on all Gerrit masters
+2. Set the first master unhealthy
+3. Shutdown the first master and [upgrade](install.html#init) to the next version
+4. Startup the first master, wait for the online reindex to complete
+5. Verify the the first master upgrade is successful and online reindex is complete
+6. Set the first master healthy
+7. Repeat steps 2. to 6. for all the other Gerrit nodes
++
+[WARNING]
+Rolling upgrade may or may not be possible depending on the changes introduced
+by the target version of the upgrade. Refer to the release notes and check whether
+the rolling upgrade is possible or not and the associated constraints.
[[gerrit.serverId]]gerrit.serverId::
+
@@ -2946,7 +2995,7 @@
[[index.batchThreads]]index.batchThreads::
+
Number of threads to use for indexing in background operations, such as
-online schema upgrades.
+online schema upgrades, and also for offline reindexing.
+
If not set or set to a zero, defaults to the number of logical CPUs as returned
by the JVM. If set to a negative value, defaults to a direct executor.
@@ -3763,6 +3812,18 @@
+
By default, 1.
+[[notedb.changes.sequenceBatchSize]]notedb.changes.sequenceBatchSize::
++
+The next available change sequence number is stored as UTF-8 text in a
+blob pointed to by the `refs/sequences/changes` ref in the `All-Projects`
+repository. Multiple processes share the same sequence by incrementing
+the counter using normal git ref updates. To amortize the cost of these
+ref updates, processes increment the counter by a larger number and
+hand out numbers from that range in memory until they run out. This
+configuration parameter controls the size of the change ID batch that
+each process retrieves at once.
++
+By default, 20.
[[oauth]]
=== Section oauth
@@ -5374,7 +5435,13 @@
login as the 'Gerrit Code Review' user, required for the link:cmd-suexec.html[suexec]
command.
-The format is one Base-64 encoded public key per line.
+The format is one Base-64 encoded public key per line with optional comment, e.g.:
+----
+# Comments allowed at start of line
+AAAAC3...51R== john@example.net
+# Another comment
+AAAAB5...21S== jane@example.net
+----
=== Configurable Parameters
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index c298ba1..a01df50 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -316,27 +316,35 @@
The submit section includes configuration of project-specific
submit settings:
-[[content_merge]]
-- 'mergeContent': Defines whether Gerrit will try to
+[[content_merge]]submit.mergeContent::
++
+Defines whether Gerrit will try to
do a content merge when a path conflict occurs. Valid values are
'true', 'false', or 'INHERIT'. Default is 'INHERIT'. This option can
be modified by any project owner through the project console, `Browse`
> `Repositories` > my/project > `Allow content merges`.
-- 'action': Defines the link:#submit-type[submit type]. Valid
+[[submit.action]]submit.action::
++
+Defines the link:#submit-type[submit type]. Valid
values are 'fast forward only', 'merge if necessary', 'rebase if necessary',
'rebase always', 'merge always' and 'cherry pick'. The default is 'merge if necessary'.
-- 'matchAuthorToCommitterDate': Defines whether to the author date will be changed to match the
-submitter date upon submit, so that git log shows when the change was submitted instead of when the
-author last committed. Valid values are 'true', 'false', or 'INHERIT'. The default is 'INHERIT'.
-This option only takes effect in submit strategies which already modify the commit, i.e.
-Cherry Pick, Rebase Always, and (perhaps) Rebase If Necessary.
+[[submit.matchAuthorToCommitterDate]]submit.matchAuthorToCommitterDate::
++
+Defines whether the author date will be changed to match the submitter date upon submit, so that
+git log shows when the change was submitted instead of when the author last committed. Valid
+values are 'true', 'false', or 'INHERIT'. The default is 'INHERIT'. This option only takes effect
+in submit strategies which already modify the commit, i.e. Cherry Pick, Rebase Always, and
+(when rebase is necessary) Rebase If Necessary.
-- 'rejectEmptyCommit': Defines whether empty commits should be rejected when a change is merged.
-Changes might not seem empty at first but when attempting to merge, rebasing can lead to an empty
-commit. If this option is set to 'true' the merge would fail. An empty commit is still allowed as
-the initial commit on a branch.
+[[submit.rejectEmptyCommit]]submit.rejectEmptyCommit::
++
+Defines whether empty commits should be rejected when a change is merged. When using
+link:#submit.action[submit action] Cherry Pick, Rebase If Necessary or Rebase Always changes may
+become empty upon submit, since the rebase|cherry-pick can lead to an empty commit. If this option
+is set to 'true' the merge would fail in such a case. An empty commit is still allowed as the
+initial commit on a branch.
[[submit-type]]
==== Submit Type
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 23ecd67..477641b 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -144,7 +144,7 @@
and everyone can comment and raise concerns.
* Design docs should stay open for a minimum of 10 calendar days so
that everyone has a fair chance to join the review.
-* Within 14 calendar days the contributor should hear back from the
+* Within 30 calendar days the contributor should hear back from the
link:dev-processes.html#steering-committee[steering committee,role=external,window=_blank]
whether the proposed feature is in scope of the project and if it can
be accepted.
diff --git a/Documentation/dev-design-docs.txt b/Documentation/dev-design-docs.txt
index e658910..7954c54 100644
--- a/Documentation/dev-design-docs.txt
+++ b/Documentation/dev-design-docs.txt
@@ -132,7 +132,7 @@
For proposed features the contributor should hear back from the
link:dev-processes.html#steering-committee[engineering steering
-committee] within 14 calendar days whether the proposed feature is in
+committee] within 30 calendar days whether the proposed feature is in
scope of the project and if it can be accepted.
[[watch-designs]]
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 8ab3d62..c8c2dff 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -391,6 +391,10 @@
same link:cmd-stream-events.html#events[events] that are also streamed
by the link:cmd-stream-events.html[gerrit stream-events] command.
+* `com.google.gerrit.extensions.events.AccountActivationListener`:
++
+User account got activated or deactivated
+
* `com.google.gerrit.extensions.events.LifecycleListener`:
+
Plugin start and stop
diff --git a/Documentation/dev-processes.txt b/Documentation/dev-processes.txt
index 3699a18..b7dd259 100644
--- a/Documentation/dev-processes.txt
+++ b/Documentation/dev-processes.txt
@@ -15,7 +15,7 @@
* ensuring timely design reviews
* ensuring that new features are compatible with the project vision and
are well aligned with other features (give feedback on new
- link:dev-design-docs.html[design docs] within 14 calendar days)
+ link:dev-design-docs.html[design docs] within 30 calendar days)
* approving/rejecting link:dev-design-docs.html[designs], vetoing new
features
* assigning link:dev-roles.html#mentor[mentors] for approved features
diff --git a/Documentation/error-no-new-changes.txt b/Documentation/error-no-new-changes.txt
index 10575fa..6434b2f 100644
--- a/Documentation/error-no-new-changes.txt
+++ b/Documentation/error-no-new-changes.txt
@@ -23,8 +23,8 @@
this simply paste the commit ID in the Gerrit Web UI into the search
field. Details about how to search in Gerrit are explained link:user-search.html[here].
-Please note that each commit can really be pushed only once. This
-means:
+Please note that generally it only makes sense for each commit to
+be pushed only once. This means:
. you cannot push a commit again even if the change for which the
commit was pushed before was abandoned (but you may restore the
@@ -32,21 +32,32 @@
. you cannot reset a change to an old patch set by pushing the old
commit for this change again
. if a commit was pushed to one branch you cannot push this commit
- to another branch in project scope.
+ to another branch in project scope (see link:user-upload.html#base[exception]).
. if a commit was pushed directly to a branch (without going through
code review) you cannot push this commit once again for code
review (please note that in this case searching by the commit ID
- in the Gerrit Web UI will not find any change)
+ in the Gerrit Web UI will not find any change), see
+ link:user-upload.html#base[exception].
If you need to re-push a commit you may rewrite this commit by
-link:http://www.kernel.org/pub/software/scm/git/docs/git-commit.html[amending,role=external,window=_blank] it or doing an interactive link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[git rebase,role=external,window=_blank]. By rewriting the
-commit you actually create a new commit (with a new commit ID in
-project scope) which can then be pushed to Gerrit.
+link:http://www.kernel.org/pub/software/scm/git/docs/git-commit.html[amending,role=external,window=_blank]
+it or doing an interactive
+link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[git rebase,role=external,window=_blank],
+or see link:user-upload.html#base[exception,role=external,window=_blank].
+By rewriting the commit you actually create a new commit
+(with a new commit ID in project scope) which can then be pushed to Gerrit.
If you are pushing the new change to the same destination branch as
the old commit (case 1 above), you also need to replace it with a new
Change-Id, otherwise the push will fail with another error message.
+Sometimes a change no longer makes sense to be destined for a specific
+branch, and instead of trying to re-push the commit for a different
+branch, it makes more sense to move the change to the preferred branch
+(where it will now likely need a rebase). Moving the change instead of
+pushing a rebased commit to the preferred branch helps to retain code
+review comments and any previous patchsets on the original change.
+
== Fast-forward merges
You will also encounter this error if you did a Fast-forward merge
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 75ad9c2..b91adbb 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -444,6 +444,23 @@
[NOTE]
Never rebase commits that are already part of a central branch.
+[[move]]
+== Move a Change
+
+Changes can be link:rest-api-changes.html#move-change[moved] to a desired
+destination branch in the same project. This is useful in cases where
+development activity switches from one branch to another and there is a
+need to move open changes on the inactive branch to the new active one.
+Another useful case is to move changes from a newer branch back to an older
+bugfix branch where an issue first appeared.
+
+Users can move a change only if they have link:access-control.html#category_abandon[
+abandon permission] on the change and link:access-control.html#category_push[
+push permission] on the destination branch.
+
+The move operation will not update the change's parent and users will have
+to link:#rebase[rebase] the change. Also, merge commits cannot be moved.
+
[[abandon]]
[[restore]]
== Abandon/Restore a Change
@@ -561,7 +578,19 @@
----
$ git push origin HEAD:refs/for/master%ready
----
-Alternatively, click *Start Review* from the Change screen.
+There are two options for marking the change ready for review from the Change
+screen:
+
+1. Click *Start Review* (the primary action *Reply* is renamed when in WIP
+state).
++
+This will open the reply-modal and allow you to add reviewers and/or CC
+before you start review.
+
+2. Click button *Mark As Active*.
++
+This will only change the state from WIP to ready, without opening the
+reply-modal.
Change owners, project owners, site administrators and members of a group that
was granted "Toggle Work In Progress state" permission can mark changes as
@@ -615,8 +644,7 @@
exposing secret details.
[[private-changes-pitfalls]]
-Pitfalls
-===
+=== Pitfalls
If private changes are used, be aware of the following pitfalls:
diff --git a/Documentation/js_licenses.txt b/Documentation/js_licenses.txt
index 97c2548..956a94d 100644
--- a/Documentation/js_licenses.txt
+++ b/Documentation/js_licenses.txt
@@ -279,36 +279,6 @@
----
-[[es6-promise]]
-es6-promise
-
-* es6-promise
-
-[[es6-promise_license]]
-----
-Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-of the Software, and to permit persons to whom the Software is furnished to do
-so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-----
-
-
[[Polymer-2018]]
Polymer-2018
@@ -747,37 +717,6 @@
----
-[[whatwg-fetch]]
-whatwg-fetch
-
-* whatwg-fetch
-
-[[whatwg-fetch_license]]
-----
-Copyright (c) 2014-2016 GitHub, Inc.
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-----
-
-
[[font-roboto-local-fonts-roboto]]
font-roboto-local-fonts-roboto
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index c1dfb94..d561596 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -3221,36 +3221,6 @@
----
-[[es6-promise]]
-es6-promise
-
-* es6-promise
-
-[[es6-promise_license]]
-----
-Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-of the Software, and to permit persons to whom the Software is furnished to do
-so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-----
-
-
[[Polymer-2018]]
Polymer-2018
@@ -3689,37 +3659,6 @@
----
-[[whatwg-fetch]]
-whatwg-fetch
-
-* whatwg-fetch
-
-[[whatwg-fetch_license]]
-----
-Copyright (c) 2014-2016 GitHub, Inc.
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-----
-
-
[[font-roboto-local-fonts-roboto]]
font-roboto-local-fonts-roboto
diff --git a/Documentation/note-db.txt b/Documentation/note-db.txt
index 0505dd2..89758a0 100644
--- a/Documentation/note-db.txt
+++ b/Documentation/note-db.txt
@@ -110,6 +110,13 @@
normally run `gerrit.war daemon` with an `-Xmx` flag, pass that to the migration
tool as well.
+[NOTE]
+Note that by appending `--reindex false` to the above command, you can skip the
+lengthy, implicit reindexing step of the migration. This is useful if you plan
+to perform further Gerrit upgrades while the server is offline and have to
+reindex later anyway (E.g.: a follow-up upgrade to Gerrit 3.2 or newer, which
+requires to reindex changes anyway).
+
*Advantages*
* Much faster than online; can use all available CPUs, since no live traffic
diff --git a/Documentation/pg-plugin-dev.txt b/Documentation/pg-plugin-dev.txt
index 1ce1d61..adb5d20 100644
--- a/Documentation/pg-plugin-dev.txt
+++ b/Documentation/pg-plugin-dev.txt
@@ -11,34 +11,27 @@
[[loading]]
== Plugin loading and initialization
-link:js-api.html#_entry_point[Entry point] for the plugin and the loading method
-is based on link:http://w3c.github.io/webcomponents/spec/imports/[HTML Imports,role=external,window=_blank]
-spec.
+link:js-api.html#_entry_point[Entry point] for the plugin.
-* The plugin provides pluginname.html, and can be a standalone file or a static
+* The plugin provides pluginname.js, and can be a standalone file or a static
asset in a jar as a link:dev-plugins.html#deployment[Web UI plugin].
-* pluginname.html contains a `dom-module` tag with a script that uses
- `Gerrit.install()`. There should only be single `Gerrit.install()` per file.
-* PolyGerrit imports pluginname.html along with all required resources defined in it
- (fonts, styles, etc).
-* For standalone plugins, the entry point file is a `pluginname.html` file
+* pluginname.js contains a call to `Gerrit.install()`. There should
+ only be single `Gerrit.install()` per file.
+* PolyGerrit imports pluginname.js.
+* For standalone plugins, the entry point file is a `pluginname.js` file
located in `gerrit-site/plugins` folder, where `pluginname` is an alphanumeric
plugin name.
Note: Code examples target modern browsers (Chrome, Firefox, Safari, Edge).
-Here's a recommended starter `myplugin.html`:
+Here's a recommended starter `myplugin.js`:
-``` html
-<dom-module id="my-plugin">
- <script>
- Gerrit.install(plugin => {
- 'use strict';
+``` js
+Gerrit.install(plugin => {
+ 'use strict';
- // Your code here.
- });
- </script>
-</dom-module>
+ // Your code here.
+});
```
[[low-level-api-concepts]]
@@ -103,34 +96,31 @@
=== Styling DOM Elements
A plugin may provide Polymer's
-https://www.polymer-project.org/2.0/docs/devguide/style-shadow-dom#style-modules[style
+https://polymer-library.polymer-project.org/3.0/docs/devguide/style-shadow-dom[style
modules,role=external,window=_blank] to style individual endpoints using
`plugin.registerStyleModule(endpointName, moduleName)`. A style must be defined
-as a standalone `<dom-module>` defined in the same .html file.
+as a standalone `<dom-module>` defined in the same .js file.
+
+See `samples/theme-plugin.js` for examples.
Note: TODO: Insert link to the full styling API.
-``` html
-<dom-module id="my-plugin">
- <script>
- Gerrit.install(plugin => {
- plugin.registerStyleModule('change-metadata', 'some-style-module');
- });
- </script>
-</dom-module>
-
-<dom-module id="some-style-module">
- <style>
+``` js
+const styleElement = document.createElement('dom-module');
+styleElement.innerHTML =
+ `<template>
+ <style>
html {
- --change-metadata-label-status: {
- display: none;
- }
- --change-metadata-strategy: {
- display: none;
- }
+ --primary-text-color: red;
}
- </style>
-</dom-module>
+ </style>
+ </template>`;
+
+styleElement.register('some-style-module');
+
+Gerrit.install(plugin => {
+ plugin.registerStyleModule('change-metadata', 'some-style-module');
+});
```
[[high-level-api-concepts]]
@@ -152,11 +142,11 @@
`plugin.attributeHelper(element)`
Alternative for
-link:https://www.polymer-project.org/1.0/docs/devguide/data-binding[Polymer data
+link:https://polymer-library.polymer-project.org/3.0/docs/devguide/data-binding[Polymer data
binding,role=external,window=_blank] for plugins that don't use Polymer. Can be used to bind element
attribute changes to callbacks.
-See `samples/bind-parameters.html` for examples on both Polymer data bindings
+See `samples/bind-parameters.js` for examples on both Polymer data bindings
and `attibuteHelper` usage.
=== eventHelper
@@ -253,26 +243,29 @@
Here's the recommended approach that uses Polymer for generating custom elements:
-``` html
-<dom-module id="some-plugin">
- <script>
- Gerrit.install(plugin => {
- plugin.registerCustomComponent(
- 'change-view-integration', 'some-ci-module');
- });
- </script>
-</dom-module>
+``` js
+class SomeCiModule extends Polymer.Element {
+ static get is() {
+ return "some-ci-module";
+ }
+ static get template() {
+ return Polymer.html`
+ Sample link: <a href="http://some.com/foo">Foo</a>
+ `;
+ }
+}
-<dom-module id="some-ci-module">
- <template>
- Sample link: <a href="http://some.com/foo">Foo</a>
- </template>
- <script>
- Polymer({is: 'some-ci-module'});
- </script>
-</dom-module>
+// Register this element
+customElements.define(SomeCiModule.is, SomeCiModule);
+
+// Install the plugin
+Gerrit.install(plugin => {
+ plugin.registerCustomComponent('change-view-integration', 'some-ci-module');
+});
```
+See `samples/` for more examples.
+
Here's a minimal example that uses low-level DOM Hooks API for the same purpose:
``` js
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 4006d1d..de6d6be 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -4471,7 +4471,7 @@
.Request
----
POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/test.submit_type HTTP/1.0
- Content-Type: text/plain; charset-UTF-8
+ Content-Type: text/plain; charset=UTF-8
submit_type(cherry_pick).
----
@@ -4502,7 +4502,7 @@
.Request
----
POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/test.submit_rule?filters=SKIP HTTP/1.0
- Content-Type: text/plain; charset-UTF-8
+ Content-Type: text/plain; charset=UTF-8
submit_rule(submit(R)) :-
R = label('Any-Label-Name', reject(_)).
@@ -6191,7 +6191,7 @@
Notify handling that defines to whom email notifications should be sent
after the cherry-pick. +
Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
-If not set, the default is `NONE`.
+If not set, the default is `ALL`.
|`notify_details` |optional|
Additional information about whom to notify about the update as a map
of recipient type to link:#notify-info[NotifyInfo] entity.
diff --git a/README.md b/README.md
index a76dac6..084f2b0 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,8 @@
[Gerrit](https://www.gerritcodereview.com) is a code review and project
management tool for Git based projects.
-[](https://gerrit-ci.gerritforge.com/job/Gerrit-master/)
+[](https://gerrit-ci.gerritforge.com/job/Gerrit-bazel-master/)
+
## Objective
diff --git a/WORKSPACE b/WORKSPACE
index e70c475..91eef76 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -60,8 +60,8 @@
http_archive(
name = "build_bazel_rules_nodejs",
- sha256 = "d14076339deb08e5460c221fae5c5e9605d2ef4848eee1f0c81c9ffdc1ab31c1",
- urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/1.6.1/rules_nodejs-1.6.1.tar.gz"],
+ sha256 = "84abf7ac4234a70924628baa9a73a5a5cbad944c4358cf9abdb4aab29c9a5b77",
+ urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/1.7.0/rules_nodejs-1.7.0.tar.gz"],
)
# File is specific to Polymer and copied from the Closure Github -- should be
@@ -82,6 +82,7 @@
# https://github.com/google/closure-templates/pull/155
rules_closure_dependencies(
omit_aopalliance = True,
+ omit_bazel_skylib = True,
omit_javax_inject = True,
omit_rules_cc = True,
)
@@ -91,10 +92,10 @@
# Golang support for PolyGerrit local dev server.
http_archive(
name = "io_bazel_rules_go",
- sha256 = "b34cbe1a7514f5f5487c3bfee7340a4496713ddf4f119f7a225583d6cafd793a",
+ sha256 = "a8d6b1b354d371a646d2f7927319974e0f9e52f73a2452d2b3877118169eb6bb",
urls = [
- "https://storage.googleapis.com/bazel-mirror/github.com/bazelbuild/rules_go/releases/download/v0.21.1/rules_go-v0.21.1.tar.gz",
- "https://github.com/bazelbuild/rules_go/releases/download/v0.21.1/rules_go-v0.21.1.tar.gz",
+ "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.23.3/rules_go-v0.23.3.tar.gz",
+ "https://github.com/bazelbuild/rules_go/releases/download/v0.23.3/rules_go-v0.23.3.tar.gz",
],
)
@@ -106,8 +107,11 @@
http_archive(
name = "bazel_gazelle",
- sha256 = "3c681998538231a2d24d0c07ed5a7658cb72bfb5fd4bf9911157c0e9ac6a2687",
- urls = ["https://github.com/bazelbuild/bazel-gazelle/releases/download/0.17.0/bazel-gazelle-0.17.0.tar.gz"],
+ sha256 = "cdb02a887a7187ea4d5a27452311a75ed8637379a1287d8eeb952138ea485f7d",
+ urls = [
+ "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.21.1/bazel-gazelle-v0.21.1.tar.gz",
+ "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.21.1/bazel-gazelle-v0.21.1.tar.gz",
+ ],
)
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")
@@ -222,26 +226,6 @@
sha1 = "42aa5155a54a87d70af32d4b0d06bf43779de0e2",
)
-FLOGGER_VERS = "0.5.1"
-
-maven_jar(
- name = "flogger",
- artifact = "com.google.flogger:flogger:" + FLOGGER_VERS,
- sha1 = "71d1e2cef9cc604800825583df56b8ef5c053f14",
-)
-
-maven_jar(
- name = "flogger-log4j-backend",
- artifact = "com.google.flogger:flogger-log4j-backend:" + FLOGGER_VERS,
- sha1 = "5e2794b75c88223f263f1c1a9d7ea51e2dc45732",
-)
-
-maven_jar(
- name = "flogger-system-backend",
- artifact = "com.google.flogger:flogger-system-backend:" + FLOGGER_VERS,
- sha1 = "b66d3bedb14da604828a8693bb24fd78e36b0e9e",
-)
-
maven_jar(
name = "gson",
artifact = "com.google.code.gson:gson:2.8.5",
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index 541e479..9d8bc57 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -39,6 +39,7 @@
"//lib:gson",
"//lib:guava-retrying",
"//lib:jgit",
+ "//lib:jgit-ssh-jsch",
"//lib:jsch",
"//lib/commons:compress",
"//lib/commons:lang",
diff --git a/java/com/google/gerrit/acceptance/EventRecorder.java b/java/com/google/gerrit/acceptance/EventRecorder.java
index 563c2ef..1618573 100644
--- a/java/com/google/gerrit/acceptance/EventRecorder.java
+++ b/java/com/google/gerrit/acceptance/EventRecorder.java
@@ -70,9 +70,9 @@
@Override
public void onEvent(Event e) {
if (e instanceof ReviewerDeletedEvent) {
- recordedEvents.put(ReviewerDeletedEvent.TYPE, (ReviewerDeletedEvent) e);
+ recordedEvents.put(ReviewerDeletedEvent.TYPE, e);
} else if (e instanceof ChangeDeletedEvent) {
- recordedEvents.put(ChangeDeletedEvent.TYPE, (ChangeDeletedEvent) e);
+ recordedEvents.put(ChangeDeletedEvent.TYPE, e);
} else if (e instanceof RefEvent) {
RefEvent event = (RefEvent) e;
String key =
diff --git a/java/com/google/gerrit/acceptance/ExtensionRegistry.java b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
index 5376d23..cfe7964 100644
--- a/java/com/google/gerrit/acceptance/ExtensionRegistry.java
+++ b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
@@ -18,6 +18,7 @@
import com.google.gerrit.extensions.config.CapabilityDefinition;
import com.google.gerrit.extensions.config.DownloadScheme;
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.ChangeIndexedListener;
import com.google.gerrit.extensions.events.CommentAddedListener;
@@ -73,6 +74,7 @@
private final DynamicSet<GroupBackend> groupBackends;
private final DynamicSet<AccountActivationValidationListener>
accountActivationValidationListeners;
+ private final DynamicSet<AccountActivationListener> accountActivationListeners;
private final DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners;
private final DynamicSet<WorkInProgressStateChangedListener> workInProgressStateChangedListeners;
private final DynamicMap<CapabilityDefinition> capabilityDefinitions;
@@ -101,6 +103,7 @@
DynamicSet<RevisionCreatedListener> revisionCreatedListeners,
DynamicSet<GroupBackend> groupBackends,
DynamicSet<AccountActivationValidationListener> accountActivationValidationListeners,
+ DynamicSet<AccountActivationListener> accountActivationListeners,
DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners,
DynamicSet<WorkInProgressStateChangedListener> workInProgressStateChangedListeners,
DynamicMap<CapabilityDefinition> capabilityDefinitions,
@@ -126,6 +129,7 @@
this.revisionCreatedListeners = revisionCreatedListeners;
this.groupBackends = groupBackends;
this.accountActivationValidationListeners = accountActivationValidationListeners;
+ this.accountActivationListeners = accountActivationListeners;
this.onSubmitValidationListeners = onSubmitValidationListeners;
this.workInProgressStateChangedListeners = workInProgressStateChangedListeners;
this.capabilityDefinitions = capabilityDefinitions;
@@ -229,6 +233,10 @@
return add(accountActivationValidationListeners, accountActivationValidationListener);
}
+ public Registration add(AccountActivationListener accountDeactivatedListener) {
+ return add(accountActivationListeners, accountDeactivatedListener);
+ }
+
public Registration add(OnSubmitValidationListener onSubmitValidationListener) {
return add(onSubmitValidationListeners, onSubmitValidationListener);
}
diff --git a/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java b/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java
index 69c1790..fb03bc5 100644
--- a/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java
@@ -24,7 +24,7 @@
public String base;
public Integer parent;
- public NotifyHandling notify = NotifyHandling.NONE;
+ public NotifyHandling notify = NotifyHandling.ALL;
public Map<RecipientType, NotifyInfo> notifyDetails;
public boolean keepReviewers;
diff --git a/java/com/google/gerrit/extensions/events/AccountActivationListener.java b/java/com/google/gerrit/extensions/events/AccountActivationListener.java
new file mode 100644
index 0000000..b45533b
--- /dev/null
+++ b/java/com/google/gerrit/extensions/events/AccountActivationListener.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/**
+ * Notified whenever an account got activated or deactivated.
+ *
+ * <p>This listener is called only after an account got (de)activated and hence cannot cancel the
+ * (de)activation. See {@link
+ * com.google.gerrit.server.validators.AccountActivationValidationListener} for a listener that can
+ * cancel a (de)activation.
+ */
+@ExtensionPoint
+public interface AccountActivationListener {
+ /**
+ * Invoked after an account got activated
+ *
+ * @param id of the account
+ */
+ default void onAccountActivated(int id) {}
+
+ /**
+ * Invoked after an account got deactivated
+ *
+ * @param id of the account
+ */
+ default void onAccountDeactivated(int id) {}
+}
diff --git a/java/com/google/gerrit/httpd/AllRequestFilter.java b/java/com/google/gerrit/httpd/AllRequestFilter.java
index 9d171d5..1c3cafe 100644
--- a/java/com/google/gerrit/httpd/AllRequestFilter.java
+++ b/java/com/google/gerrit/httpd/AllRequestFilter.java
@@ -18,6 +18,8 @@
import com.google.gerrit.server.plugins.Plugin;
import com.google.gerrit.server.plugins.StopPluginListener;
import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Scopes;
import com.google.inject.Singleton;
import com.google.inject.internal.UniqueAnnotations;
import com.google.inject.servlet.ServletModule;
@@ -32,11 +34,15 @@
/** Filters all HTTP requests passing through the server. */
public abstract class AllRequestFilter implements Filter {
- public static ServletModule module() {
+ public static Module module() {
return new ServletModule() {
@Override
protected void configureServlets() {
DynamicSet.setOf(binder(), AllRequestFilter.class);
+ DynamicSet.bind(binder(), AllRequestFilter.class)
+ .to(AllowRenderInFrameFilter.class)
+ .in(Scopes.SINGLETON);
+
filter("/*").through(FilterProxy.class);
bind(StopPluginListener.class)
diff --git a/java/com/google/gerrit/httpd/AllowRenderInFrameFilter.java b/java/com/google/gerrit/httpd/AllowRenderInFrameFilter.java
new file mode 100644
index 0000000..b414aad
--- /dev/null
+++ b/java/com/google/gerrit/httpd/AllowRenderInFrameFilter.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd;
+
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import java.io.IOException;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.lib.Config;
+
+public class AllowRenderInFrameFilter extends AllRequestFilter {
+ static final String X_FRAME_OPTIONS_HEADER_NAME = "X-Frame-Options";
+
+ public static enum XFrameOption {
+ ALLOW,
+ SAMEORIGIN;
+ }
+
+ private final String xframeOptionString;
+ private final boolean skipXFrameOption;
+
+ @Inject
+ public AllowRenderInFrameFilter(@GerritServerConfig Config cfg) {
+ XFrameOption xframeOption =
+ cfg.getEnum("gerrit", null, "xframeOption", XFrameOption.SAMEORIGIN);
+ boolean canLoadInIFrame = cfg.getBoolean("gerrit", "canLoadInIFrame", false);
+ xframeOptionString = canLoadInIFrame ? xframeOption.name() : "DENY";
+
+ skipXFrameOption = xframeOption.equals(XFrameOption.ALLOW) && canLoadInIFrame;
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+ if (skipXFrameOption) {
+ chain.doFilter(request, response);
+ } else {
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+ httpResponse.addHeader(X_FRAME_OPTIONS_HEADER_NAME, xframeOptionString);
+ chain.doFilter(request, httpResponse);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index 7f878aa..5c4830c 100644
--- a/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -16,6 +16,7 @@
import static java.util.concurrent.TimeUnit.HOURS;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
@@ -27,6 +28,7 @@
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.config.AuthConfig;
@@ -40,7 +42,7 @@
@RequestScoped
public abstract class CacheBasedWebSession implements WebSession {
- private static final String ACCOUNT_COOKIE = "GerritAccount";
+ @VisibleForTesting public static final String ACCOUNT_COOKIE = "GerritAccount";
protected static final long MAX_AGE_MINUTES = HOURS.toMinutes(12);
private final HttpServletRequest request;
@@ -50,6 +52,7 @@
private final Provider<AnonymousUser> anonymousProvider;
private final IdentifiedUser.RequestFactory identified;
private final EnumSet<AccessPath> okPaths = EnumSet.of(AccessPath.UNKNOWN);
+ private final AccountCache byIdCache;
private Cookie outCookie;
private Key key;
@@ -62,13 +65,15 @@
WebSessionManager manager,
AuthConfig authConfig,
Provider<AnonymousUser> anonymousProvider,
- IdentifiedUser.RequestFactory identified) {
+ IdentifiedUser.RequestFactory identified,
+ AccountCache byIdCache) {
this.request = request;
this.response = response;
this.manager = manager;
this.authConfig = authConfig;
this.anonymousProvider = anonymousProvider;
this.identified = identified;
+ this.byIdCache = byIdCache;
if (request.getRequestURI() == null || !GitSmartHttpTools.isGitClient(request)) {
String cookie = readCookie(request);
@@ -85,6 +90,10 @@
authFromQueryParameter(token);
}
}
+ if (val != null && !checkAccountStatus(val.getAccountId())) {
+ val = null;
+ okPaths.clear();
+ }
if (val != null && val.needsCookieRefresh()) {
// Session is more than half old; update cache entry with new expiration date.
val = manager.createVal(key, val);
@@ -177,6 +186,11 @@
manager.destroy(key);
}
+ if (!checkAccountStatus(id)) {
+ val = null;
+ return;
+ }
+
key = manager.createKey(id);
val = manager.createVal(key, id, rememberMe, identity, null, null);
saveCookie();
@@ -207,6 +221,10 @@
return val != null ? val.getSessionId() : null;
}
+ private boolean checkAccountStatus(Account.Id id) {
+ return byIdCache.get(id).filter(as -> as.account().isActive()).isPresent();
+ }
+
private void saveCookie() {
if (response == null) {
return;
diff --git a/java/com/google/gerrit/httpd/H2CacheBasedWebSession.java b/java/com/google/gerrit/httpd/H2CacheBasedWebSession.java
index caced27..830d8d6 100644
--- a/java/com/google/gerrit/httpd/H2CacheBasedWebSession.java
+++ b/java/com/google/gerrit/httpd/H2CacheBasedWebSession.java
@@ -20,6 +20,7 @@
import com.google.gerrit.httpd.WebSessionManager.Val;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.IdentifiedUser.RequestFactory;
+import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.config.AuthConfig;
import com.google.inject.Inject;
@@ -59,8 +60,15 @@
@Named(WebSessionManager.CACHE_NAME) Cache<String, Val> cache,
AuthConfig authConfig,
Provider<AnonymousUser> anonymousProvider,
- RequestFactory identified) {
+ RequestFactory identified,
+ AccountCache byIdCache) {
super(
- request, response, managerFactory.create(cache), authConfig, anonymousProvider, identified);
+ request,
+ response,
+ managerFactory.create(cache),
+ authConfig,
+ anonymousProvider,
+ identified,
+ byIdCache);
}
}
diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
index 41d2f83..cddaea4 100644
--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
@@ -153,8 +153,7 @@
serializeObject(GSON, accountApi.getEditPreferences()));
data.put("userIsAuthenticated", true);
} catch (AuthException e) {
- logger.atFine().withCause(e).log(
- "Can't inline account-related data because user is unauthenticated");
+ logger.atFine().log("Can't inline account-related data because user is unauthenticated");
// Don't render data
}
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 89ebdc1..f743578 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -601,6 +601,7 @@
}
} catch (MalformedJsonException | JsonParseException e) {
cause = Optional.of(e);
+ logger.atFine().withCause(e).log("REST call failed on JSON parsing");
responseBytes =
replyError(
req, res, statusCode = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e);
diff --git a/java/com/google/gerrit/index/Schema.java b/java/com/google/gerrit/index/Schema.java
index 0aa374b..3aa9de0 100644
--- a/java/com/google/gerrit/index/Schema.java
+++ b/java/com/google/gerrit/index/Schema.java
@@ -22,6 +22,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.StorageException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -176,6 +177,33 @@
return true;
}
+ private Values<T> fieldValues(T obj, FieldDef<T, ?> f, ImmutableSet<String> skipFields) {
+ if (skipFields.contains(f.getName())) {
+ return null;
+ }
+
+ Object v;
+ try {
+ v = f.get(obj);
+ } catch (StorageException e) {
+ // StorageException is thrown when the object is not found. On this case,
+ // it is pointless to make further attempts for each field, so propagate
+ // the exception to return an empty list.
+ logger.atSevere().withCause(e).log("error getting field %s of %s", f.getName(), obj);
+ throw e;
+ } catch (RuntimeException e) {
+ logger.atSevere().withCause(e).log("error getting field %s of %s", f.getName(), obj);
+ return null;
+ }
+ if (v == null) {
+ return null;
+ } else if (f.isRepeatable()) {
+ return new Values<>(f, (Iterable<?>) v);
+ } else {
+ return new Values<>(f, Collections.singleton(v));
+ }
+ }
+
/**
* Build all fields in the schema from an input object.
*
@@ -186,31 +214,14 @@
* @return all non-null field values from the object.
*/
public final Iterable<Values<T>> buildFields(T obj, ImmutableSet<String> skipFields) {
- return fields.values().stream()
- .map(
- f -> {
- if (skipFields.contains(f.getName())) {
- return null;
- }
-
- Object v;
- try {
- v = f.get(obj);
- } catch (RuntimeException e) {
- logger.atSevere().withCause(e).log(
- "error getting field %s of %s", f.getName(), obj);
- return null;
- }
- if (v == null) {
- return null;
- } else if (f.isRepeatable()) {
- return new Values<>(f, (Iterable<?>) v);
- } else {
- return new Values<>(f, Collections.singleton(v));
- }
- })
- .filter(Objects::nonNull)
- .collect(toImmutableList());
+ try {
+ return fields.values().stream()
+ .map(f -> fieldValues(obj, f, skipFields))
+ .filter(Objects::nonNull)
+ .collect(toImmutableList());
+ } catch (StorageException e) {
+ return ImmutableList.of();
+ }
}
@Override
diff --git a/java/com/google/gerrit/index/SiteIndexer.java b/java/com/google/gerrit/index/SiteIndexer.java
index ecfc7bd..32b4b21 100644
--- a/java/com/google/gerrit/index/SiteIndexer.java
+++ b/java/com/google/gerrit/index/SiteIndexer.java
@@ -83,7 +83,7 @@
}
protected PrintWriter newPrintWriter(OutputStream out) {
- return new PrintWriter(new OutputStreamWriter(out, UTF_8));
+ return new PrintWriter(new OutputStreamWriter(out, UTF_8), true);
}
private static class ErrorListener implements Runnable {
diff --git a/java/com/google/gerrit/pgm/Init.java b/java/com/google/gerrit/pgm/Init.java
index 3593d8a..4e62a0f 100644
--- a/java/com/google/gerrit/pgm/Init.java
+++ b/java/com/google/gerrit/pgm/Init.java
@@ -22,6 +22,7 @@
import com.google.gerrit.common.IoUtil;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.PluginData;
+import com.google.gerrit.index.SchemaDefinitions;
import com.google.gerrit.index.project.ProjectSchemaDefinitions;
import com.google.gerrit.pgm.init.BaseInit;
import com.google.gerrit.pgm.init.Browser;
@@ -31,6 +32,9 @@
import com.google.gerrit.server.config.GerritServerConfigModule;
import com.google.gerrit.server.config.SitePath;
import com.google.gerrit.server.index.GerritIndexStatus;
+import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
+import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
+import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
import com.google.gerrit.server.ioutil.HostPlatform;
import com.google.gerrit.server.securestore.SecureStoreClassName;
import com.google.gerrit.server.util.ReplicaUtil;
@@ -89,7 +93,7 @@
@Inject Browser browser;
- private boolean projectsIndexExists;
+ private GerritIndexStatus indexStatus;
public Init() {
super(new WarDistribution(), null);
@@ -103,7 +107,7 @@
@Override
protected boolean beforeInit(SiteInit init) throws Exception {
- projectsIndexExists = new GerritIndexStatus(init.site).exists(ProjectSchemaDefinitions.NAME);
+ indexStatus = new GerritIndexStatus(init.site);
ErrorLogFile.errorOnlyConsole();
if (!skipPlugins) {
@@ -132,6 +136,12 @@
@Override
protected void afterInit(SiteRun run) throws Exception {
+ List<SchemaDefinitions<?>> schemaDefs =
+ ImmutableList.of(
+ AccountSchemaDefinitions.INSTANCE,
+ ChangeSchemaDefinitions.INSTANCE,
+ GroupSchemaDefinitions.INSTANCE,
+ ProjectSchemaDefinitions.INSTANCE);
List<Module> modules = new ArrayList<>();
modules.add(
new AbstractModule() {
@@ -146,8 +156,12 @@
});
modules.add(new GerritServerConfigModule());
Guice.createInjector(modules).injectMembers(this);
- if (!ReplicaUtil.isReplica(run.flags.cfg) && !projectsIndexExists) {
- reindexProjects();
+ if (!ReplicaUtil.isReplica(run.flags.cfg)) {
+ for (SchemaDefinitions<?> schemaDef : schemaDefs) {
+ if (!indexStatus.exists(schemaDef.getName())) {
+ reindex(schemaDef);
+ }
+ }
}
start(run);
}
@@ -260,8 +274,7 @@
}
}
- private void reindexProjects() throws Exception {
- // Reindex all projects, so that we bootstrap the project index for new installations
+ private void reindex(SchemaDefinitions<?> schemaDef) throws Exception {
List<String> reindexArgs =
ImmutableList.of(
"--site-path",
@@ -269,8 +282,9 @@
"--threads",
Integer.toString(1),
"--index",
- ProjectSchemaDefinitions.NAME);
- getConsoleUI().message("Init complete, reindexing projects with:");
+ schemaDef.getName());
+ getConsoleUI()
+ .message(String.format("Init complete, reindexing %s with:", schemaDef.getName()));
getConsoleUI().message(" reindex " + reindexArgs.stream().collect(joining(" ")));
Reindex reindexPgm = new Reindex();
reindexPgm.main(reindexArgs.stream().toArray(String[]::new));
diff --git a/java/com/google/gerrit/pgm/Reindex.java b/java/com/google/gerrit/pgm/Reindex.java
index 2e526bb..966801f 100644
--- a/java/com/google/gerrit/pgm/Reindex.java
+++ b/java/com/google/gerrit/pgm/Reindex.java
@@ -202,6 +202,9 @@
if (result.success()) {
index.markReady(true);
}
+ System.out.format(
+ "Index %s in version %d is %sready\n",
+ def.getName(), index.getSchema().getVersion(), result.success() ? "" : "NOT ");
return result.success();
}
}
diff --git a/java/com/google/gerrit/server/ChangeMessagesUtil.java b/java/com/google/gerrit/server/ChangeMessagesUtil.java
index dd48b93..32edadb 100644
--- a/java/com/google/gerrit/server/ChangeMessagesUtil.java
+++ b/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -35,34 +35,38 @@
@Singleton
public class ChangeMessagesUtil {
public static final String AUTOGENERATED_TAG_PREFIX = "autogenerated:";
+ public static final String AUTOGENERATED_BY_GERRIT_TAG_PREFIX =
+ AUTOGENERATED_TAG_PREFIX + "gerrit:";
- public static final String TAG_ABANDON = AUTOGENERATED_TAG_PREFIX + "gerrit:abandon";
+ public static final String TAG_ABANDON = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "abandon";
public static final String TAG_CHERRY_PICK_CHANGE =
- AUTOGENERATED_TAG_PREFIX + "gerrit:cherryPickChange";
+ AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "cherryPickChange";
public static final String TAG_DELETE_ASSIGNEE =
- AUTOGENERATED_TAG_PREFIX + "gerrit:deleteAssignee";
+ AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "deleteAssignee";
public static final String TAG_DELETE_REVIEWER =
- AUTOGENERATED_TAG_PREFIX + "gerrit:deleteReviewer";
- public static final String TAG_DELETE_VOTE = AUTOGENERATED_TAG_PREFIX + "gerrit:deleteVote";
- public static final String TAG_MERGED = AUTOGENERATED_TAG_PREFIX + "gerrit:merged";
- public static final String TAG_MOVE = AUTOGENERATED_TAG_PREFIX + "gerrit:move";
- public static final String TAG_RESTORE = AUTOGENERATED_TAG_PREFIX + "gerrit:restore";
- public static final String TAG_REVERT = AUTOGENERATED_TAG_PREFIX + "gerrit:revert";
- public static final String TAG_SET_ASSIGNEE = AUTOGENERATED_TAG_PREFIX + "gerrit:setAssignee";
+ AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "deleteReviewer";
+ public static final String TAG_DELETE_VOTE = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "deleteVote";
+ public static final String TAG_MERGED = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "merged";
+ public static final String TAG_MOVE = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "move";
+ public static final String TAG_RESTORE = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "restore";
+ public static final String TAG_REVERT = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "revert";
+ public static final String TAG_SET_ASSIGNEE = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setAssignee";
public static final String TAG_UPDATE_ATTENTION_SET =
- AUTOGENERATED_TAG_PREFIX + "gerrit:updateAttentionSet";
+ AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "updateAttentionSet";
public static final String TAG_SET_DESCRIPTION =
- AUTOGENERATED_TAG_PREFIX + "gerrit:setPsDescription";
- public static final String TAG_SET_HASHTAGS = AUTOGENERATED_TAG_PREFIX + "gerrit:setHashtag";
- public static final String TAG_SET_PRIVATE = AUTOGENERATED_TAG_PREFIX + "gerrit:setPrivate";
- public static final String TAG_SET_READY = AUTOGENERATED_TAG_PREFIX + "gerrit:setReadyForReview";
- public static final String TAG_SET_TOPIC = AUTOGENERATED_TAG_PREFIX + "gerrit:setTopic";
- public static final String TAG_SET_WIP = AUTOGENERATED_TAG_PREFIX + "gerrit:setWorkInProgress";
- public static final String TAG_UNSET_PRIVATE = AUTOGENERATED_TAG_PREFIX + "gerrit:unsetPrivate";
+ AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setPsDescription";
+ public static final String TAG_SET_HASHTAGS = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setHashtag";
+ public static final String TAG_SET_PRIVATE = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setPrivate";
+ public static final String TAG_SET_READY =
+ AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setReadyForReview";
+ public static final String TAG_SET_TOPIC = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setTopic";
+ public static final String TAG_SET_WIP = AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "setWorkInProgress";
+ public static final String TAG_UNSET_PRIVATE =
+ AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "unsetPrivate";
public static final String TAG_UPLOADED_PATCH_SET =
- AUTOGENERATED_TAG_PREFIX + "gerrit:newPatchSet";
+ AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "newPatchSet";
public static final String TAG_UPLOADED_WIP_PATCH_SET =
- AUTOGENERATED_TAG_PREFIX + "gerrit:newWipPatchSet";
+ AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "newWipPatchSet";
public static ChangeMessage newMessage(ChangeContext ctx, String body, @Nullable String tag) {
return newMessage(ctx.getChange().currentPatchSetId(), ctx.getUser(), ctx.getWhen(), body, tag);
@@ -122,6 +126,10 @@
return tag != null && tag.startsWith(AUTOGENERATED_TAG_PREFIX);
}
+ public static boolean isAutogeneratedByGerrit(@Nullable String tag) {
+ return tag != null && tag.startsWith(AUTOGENERATED_BY_GERRIT_TAG_PREFIX);
+ }
+
public static ChangeMessageInfo createChangeMessageInfo(
ChangeMessage message, AccountLoader accountLoader) {
PatchSet.Id patchNum = message.getPatchSetId();
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index 450cbe0..e8b44aa 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -272,7 +272,9 @@
}
private static boolean isAutoGenerated(ChangeMessage cm) {
- return ChangeMessagesUtil.isAutogenerated(cm.getTag());
+ // Ignore Gerrit auto-generated messages, allowing to link against human change messages that
+ // have an auto-generated tag
+ return ChangeMessagesUtil.isAutogeneratedByGerrit(cm.getTag());
}
private static boolean isAfter(CommentInfo c, ChangeMessage cm) {
diff --git a/java/com/google/gerrit/server/account/SetInactiveFlag.java b/java/com/google/gerrit/server/account/SetInactiveFlag.java
index 32ed694..4b68198 100644
--- a/java/com/google/gerrit/server/account/SetInactiveFlag.java
+++ b/java/com/google/gerrit/server/account/SetInactiveFlag.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.account;
import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.events.AccountActivationListener;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
@@ -38,13 +39,16 @@
private final PluginSetContext<AccountActivationValidationListener>
accountActivationValidationListeners;
private final Provider<AccountsUpdate> accountsUpdateProvider;
+ private final PluginSetContext<AccountActivationListener> accountActivationListeners;
@Inject
SetInactiveFlag(
PluginSetContext<AccountActivationValidationListener> accountActivationValidationListeners,
- @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider) {
+ @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider,
+ PluginSetContext<AccountActivationListener> accountActivationListeners) {
this.accountActivationValidationListeners = accountActivationValidationListeners;
this.accountsUpdateProvider = accountsUpdateProvider;
+ this.accountActivationListeners = accountActivationListeners;
}
public Response<?> deactivate(Account.Id accountId)
@@ -77,6 +81,12 @@
if (alreadyInactive.get()) {
throw new ResourceConflictException("account not active");
}
+
+ // At this point the account got set inactive and no errors occurred
+
+ int id = accountId.get();
+ accountActivationListeners.runEach(l -> l.onAccountDeactivated(id));
+
return Response.none();
}
@@ -107,6 +117,16 @@
if (exception.get().isPresent()) {
throw exception.get().get();
}
- return alreadyActive.get() ? Response.ok() : Response.created();
+
+ Response<String> res;
+ if (alreadyActive.get()) {
+ res = Response.ok();
+ } else {
+ res = Response.created();
+
+ int id = accountId.get();
+ accountActivationListeners.runEach(l -> l.onAccountActivated(id));
+ }
+ return res;
}
}
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerOp.java b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
index 3d43f59..5a9fb99 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -218,7 +218,7 @@
ChangeMessage changeMessage,
NotifyResolver.Result notify,
RepoView repoView)
- throws EmailException, IOException {
+ throws EmailException {
Account.Id userId = user.get().getAccountId();
if (userId.equals(reviewer.account().id())) {
// The user knows they removed themselves, don't bother emailing them.
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index e447f2b..cf592bf 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -31,6 +31,7 @@
import com.google.gerrit.extensions.config.ExternalIncludedIn;
import com.google.gerrit.extensions.config.FactoryModule;
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.AgreementSignupListener;
import com.google.gerrit.extensions.events.AssigneeChangedListener;
@@ -343,6 +344,7 @@
DynamicSet.setOf(binder(), PostReceiveHook.class);
DynamicSet.setOf(binder(), PreUploadHook.class);
DynamicSet.setOf(binder(), PostUploadHook.class);
+ DynamicSet.setOf(binder(), AccountActivationListener.class);
DynamicSet.setOf(binder(), AccountIndexedListener.class);
DynamicSet.setOf(binder(), ChangeIndexedListener.class);
DynamicSet.setOf(binder(), GroupIndexedListener.class);
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 7535f51..923ba68 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -45,6 +45,9 @@
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.ValidationError;
import com.google.gerrit.server.git.validators.ValidationMessage.Type;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.RefPermission;
@@ -230,7 +233,17 @@
List<CommitValidationMessage> messages = new ArrayList<>();
try {
for (CommitValidationListener commitValidator : validators) {
- messages.addAll(commitValidator.onCommitReceived(receiveEvent));
+ try (TraceTimer traceTimer =
+ TraceContext.newTimer(
+ "Running CommitValidationListener",
+ Metadata.builder()
+ .className(commitValidator.getClass().getSimpleName())
+ .projectName(receiveEvent.getProjectNameKey().get())
+ .branchName(receiveEvent.getBranchNameKey().branch())
+ .commit(receiveEvent.commit.name())
+ .build())) {
+ messages.addAll(commitValidator.onCommitReceived(receiveEvent));
+ }
}
} catch (CommitValidationException e) {
logger.atFine().withCause(e).log(
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index 2a8a5ba..e9349c4 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -21,7 +21,6 @@
import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
import com.google.common.base.Stopwatch;
-import com.google.common.collect.ComparisonChain;
import com.google.common.flogger.FluentLogger;
import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.ListenableFuture;
@@ -43,10 +42,9 @@
import com.google.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Objects;
-import java.util.SortedSet;
-import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -62,6 +60,7 @@
*/
public class AllChangesIndexer extends SiteIndexer<Change.Id, ChangeData, ChangeIndex> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final int PROJECT_SLICE_MAX_REFS = 1000;
private final ChangeData.Factory changeDataFactory;
private final GitRepositoryManager repoManager;
@@ -86,22 +85,27 @@
this.projectCache = projectCache;
}
- private static class ProjectHolder implements Comparable<ProjectHolder> {
- final Project.NameKey name;
- private final long size;
+ private static class ProjectSlice {
+ private final Project.NameKey name;
+ private final int slice;
+ private final int slices;
- ProjectHolder(Project.NameKey name, long size) {
+ ProjectSlice(Project.NameKey name, int slice, int slices) {
this.name = name;
- this.size = size;
+ this.slice = slice;
+ this.slices = slices;
}
- @Override
- public int compareTo(ProjectHolder other) {
- // Sort projects based on size first to maximize utilization of threads early on.
- return ComparisonChain.start()
- .compare(other.size, size)
- .compare(other.name.get(), name.get())
- .result();
+ public Project.NameKey getName() {
+ return name;
+ }
+
+ public int getSlice() {
+ return slice;
+ }
+
+ public int getSlices() {
+ return slices;
}
}
@@ -109,19 +113,39 @@
public Result indexAll(ChangeIndex index) {
ProgressMonitor pm = new TextProgressMonitor();
pm.beginTask("Collecting projects", ProgressMonitor.UNKNOWN);
- SortedSet<ProjectHolder> projects = new TreeSet<>();
+ List<ProjectSlice> projectSlices = new ArrayList<>();
int changeCount = 0;
Stopwatch sw = Stopwatch.createStarted();
int projectsFailed = 0;
for (Project.NameKey name : projectCache.all()) {
try (Repository repo = repoManager.openRepository(name)) {
+ // The simplest approach to distribute indexing would be to let each thread grab a project
+ // and index it fully. But if a site has one big project and 100s of small projects, then
+ // in the beginning all CPUs would be busy reindexing projects. But soon enough all small
+ // projects have been reindexed, and only the thread that reindexes the big project is
+ // still working. The other threads would idle. Reindexing the big project on a single
+ // thread becomes the critical path. Bringing in more CPUs would not speed up things.
+ //
+ // To avoid such situations, we split big repos into smaller parts and let
+ // the thread pool index these smaller parts. This splitting introduces an overhead in the
+ // workload setup and there might be additional slow-downs from multiple threads
+ // concurrently working on different parts of the same project. But for Wikimedia's Gerrit,
+ // which had 2 big projects, many middle sized ones, and lots of smaller ones, the
+ // splitting of repos into smaller parts reduced indexing time from 1.5 hours to 55 minutes
+ // in 2020.
int size = estimateSize(repo);
changeCount += size;
- projects.add(new ProjectHolder(name, size));
+ int slices = 1 + size / PROJECT_SLICE_MAX_REFS;
+ if (slices > 1) {
+ verboseWriter.println("Submitting " + name + " for indexing in " + slices + " slices");
+ }
+ for (int slice = 0; slice < slices; slice++) {
+ projectSlices.add(new ProjectSlice(name, slice, slices));
+ }
} catch (IOException e) {
logger.atSevere().withCause(e).log("Error collecting project %s", name);
projectsFailed++;
- if (projectsFailed > projects.size() / 2) {
+ if (projectsFailed > projectCache.all().size() / 2) {
logger.atSevere().log("Over 50%% of the projects could not be collected: aborted");
return Result.create(sw, false, 0, 0);
}
@@ -130,7 +154,15 @@
}
pm.endTask();
setTotalWork(changeCount);
- return indexAll(index, projects);
+
+ // projectSlices are currently grouped by projects. First all slices for project1, followed
+ // by all slices for project2, and so on. As workers pick tasks sequentially, multiple threads
+ // would typically work concurrently on different slices of the same project. While this is not
+ // a big issue, shuffling the list beforehand helps with ungrouping the project slices, so
+ // different slices are less likely to be worked on concurrently.
+ // This shuffling gave a 6% runtime reduction for Wikimedia's Gerrit in 2020.
+ Collections.shuffle(projectSlices);
+ return indexAll(index, projectSlices);
}
private int estimateSize(Repository repo) throws IOException {
@@ -146,10 +178,10 @@
return Ints.saturatedCast(size);
}
- private SiteIndexer.Result indexAll(ChangeIndex index, SortedSet<ProjectHolder> projects) {
+ private SiteIndexer.Result indexAll(ChangeIndex index, List<ProjectSlice> projectSlices) {
Stopwatch sw = Stopwatch.createStarted();
MultiProgressMonitor mpm = new MultiProgressMonitor(progressOut, "Reindexing changes");
- Task projTask = mpm.beginSubTask("projects", projects.size());
+ Task projTask = mpm.beginSubTask("project-slices", projectSlices.size());
checkState(totalWork >= 0);
Task doneTask = mpm.beginSubTask(null, totalWork);
Task failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
@@ -157,12 +189,21 @@
List<ListenableFuture<?>> futures = new ArrayList<>();
AtomicBoolean ok = new AtomicBoolean(true);
- for (ProjectHolder project : projects) {
+ for (ProjectSlice projectSlice : projectSlices) {
+ Project.NameKey name = projectSlice.getName();
+ int slice = projectSlice.getSlice();
+ int slices = projectSlice.getSlices();
ListenableFuture<?> future =
executor.submit(
reindexProject(
- indexerFactory.create(executor, index), project.name, doneTask, failedTask));
- addErrorListener(future, "project " + project.name, projTask, ok);
+ indexerFactory.create(executor, index),
+ name,
+ slice,
+ slices,
+ doneTask,
+ failedTask));
+ String description = "project " + name + " (" + slice + "/" + slices + ")";
+ addErrorListener(future, description, projTask, ok);
futures.add(future);
}
@@ -197,22 +238,38 @@
public Callable<Void> reindexProject(
ChangeIndexer indexer, Project.NameKey project, Task done, Task failed) {
- return new ProjectIndexer(indexer, project, done, failed);
+ return reindexProject(indexer, project, 0, 1, done, failed);
+ }
+
+ public Callable<Void> reindexProject(
+ ChangeIndexer indexer,
+ Project.NameKey project,
+ int slice,
+ int slices,
+ Task done,
+ Task failed) {
+ return new ProjectIndexer(indexer, project, slice, slices, done, failed);
}
private class ProjectIndexer implements Callable<Void> {
private final ChangeIndexer indexer;
private final Project.NameKey project;
+ private final int slice;
+ private final int slices;
private final ProgressMonitor done;
private final ProgressMonitor failed;
private ProjectIndexer(
ChangeIndexer indexer,
Project.NameKey project,
+ int slice,
+ int slices,
ProgressMonitor done,
ProgressMonitor failed) {
this.indexer = indexer;
this.project = project;
+ this.slice = slice;
+ this.slices = slices;
this.done = done;
this.failed = failed;
}
@@ -227,7 +284,7 @@
// It does mean that reindexing after invalidating the DiffSummary cache will be expensive,
// but the goal is to invalidate that cache as infrequently as we possibly can. And besides,
// we don't have concrete proof that improving packfile locality would help.
- notesFactory.scan(repo, project).forEach(r -> index(r));
+ notesFactory.scan(repo, project, id -> (id.get() % slices) == slice).forEach(r -> index(r));
} catch (RepositoryNotFoundException rnfe) {
logger.atSevere().log(rnfe.getMessage());
} finally {
@@ -244,7 +301,8 @@
try {
indexer.index(changeDataFactory.create(r.notes()));
done.update(1);
- verboseWriter.println("Reindexed change " + r.id());
+ verboseWriter.format(
+ "Reindexed change %d (project: %s)\n", r.id().get(), r.notes().getProjectName().get());
} catch (RejectedExecutionException e) {
// Server shutdown, don't spam the logs.
failSilently();
diff --git a/java/com/google/gerrit/server/logging/Metadata.java b/java/com/google/gerrit/server/logging/Metadata.java
index 0a99d83..1a4a335 100644
--- a/java/com/google/gerrit/server/logging/Metadata.java
+++ b/java/com/google/gerrit/server/logging/Metadata.java
@@ -66,6 +66,9 @@
/** The cause of an error. */
public abstract Optional<String> cause();
+ /** The SHA1 of a commit. */
+ public abstract Optional<String> commit();
+
/** The type of an event. */
public abstract Optional<String> eventType();
@@ -282,6 +285,8 @@
public abstract Builder cause(@Nullable String cause);
+ public abstract Builder commit(@Nullable String commit);
+
public abstract Builder eventType(@Nullable String eventType);
public abstract Builder exportValue(@Nullable String exportValue);
diff --git a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
index 5b3b34c..bca5338 100644
--- a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
+++ b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
@@ -50,7 +50,7 @@
setHeader("Subject", "[Gerrit Code Review] HTTP password was " + operation);
setMessageId(
messageIdGenerator.fromReasonAccountIdAndTimestamp(
- "HTTP password change", user.getAccountId(), TimeUtil.nowTs()));
+ "HTTP_password_change", user.getAccountId(), TimeUtil.now()));
add(RecipientType.TO, Address.create(getEmail()));
}
diff --git a/java/com/google/gerrit/server/mail/send/MessageIdGenerator.java b/java/com/google/gerrit/server/mail/send/MessageIdGenerator.java
index 01e165d..3a411dc 100644
--- a/java/com/google/gerrit/server/mail/send/MessageIdGenerator.java
+++ b/java/com/google/gerrit/server/mail/send/MessageIdGenerator.java
@@ -28,7 +28,7 @@
import com.google.gerrit.server.update.RepoView;
import com.google.inject.Inject;
import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
import java.util.Optional;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
@@ -111,7 +111,7 @@
* @return MessageId that depends on the reason, accountId, and timestamp.
*/
public MessageId fromReasonAccountIdAndTimestamp(
- String reason, Account.Id accountId, Timestamp timestamp) {
+ String reason, Account.Id accountId, Instant timestamp) {
return new AutoValue_MessageIdGenerator_MessageId(
reason + "-" + accountId.toString() + "-" + timestamp.toString());
}
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index fdd6b81..8f63177 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -259,9 +259,12 @@
return true;
}
+ // All message ids must start with < and end with >. Also, they must have @domain and no spaces.
private void addMessageId(OutgoingEmailValidationListener.Args va, String suffix) {
if (messageId != null) {
- va.headers.put(FieldName.MESSAGE_ID, new EmailHeader.String(messageId.id() + suffix));
+ String message = "<" + messageId.id() + suffix + "@" + getGerritHost() + ">";
+ message = message.replaceAll("\\s", "");
+ va.headers.put(FieldName.MESSAGE_ID, new EmailHeader.String(message));
}
}
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index 7136d2b..8e6606e 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -110,7 +110,7 @@
ChangeNoteUtil noteUtil, PersonIdent serverIdent, CurrentUser u, Date when) {
checkUserType(u);
if (u instanceof IdentifiedUser) {
- return noteUtil.newIdent(u.asIdentifiedUser().getAccount().id(), when, serverIdent);
+ return noteUtil.newAccountIdIdent(u.asIdentifiedUser().getAccount().id(), when, serverIdent);
} else if (u instanceof InternalUser) {
return serverIdent;
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index 86b6ed7..15f187a 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -69,16 +69,38 @@
return changeNoteJson;
}
- public PersonIdent newIdent(Account.Id accountId, Date when, PersonIdent serverIdent) {
- return new PersonIdent(
- getUsername(accountId), getEmailAddress(accountId), when, serverIdent.getTimeZone());
+ /**
+ * Generates a user identifier that contains the account ID, but not the user's name or email
+ * address.
+ *
+ * @return The passed in {@link StringBuilder} instance to which the identifier has been appended.
+ */
+ StringBuilder appendAccountIdIdentString(StringBuilder stringBuilder, Account.Id accountId) {
+ return stringBuilder
+ .append(getAccountIdAsUsername(accountId))
+ .append(" <")
+ .append(getAccountIdAsEmailAddress(accountId))
+ .append('>');
}
- private static String getUsername(Account.Id accountId) {
+ /**
+ * Returns a {@link PersonIdent} that contains the account ID, but not the user's name or email
+ * address.
+ */
+ public PersonIdent newAccountIdIdent(Account.Id accountId, Date when, PersonIdent serverIdent) {
+ return new PersonIdent(
+ getAccountIdAsUsername(accountId),
+ getAccountIdAsEmailAddress(accountId),
+ when,
+ serverIdent.getTimeZone());
+ }
+
+ /** Returns the string {@code "Gerrit User " + accountId}, to pseudonymize user names. */
+ public static String getAccountIdAsUsername(Account.Id accountId) {
return "Gerrit User " + accountId.toString();
}
- private String getEmailAddress(Account.Id accountId) {
+ private String getAccountIdAsEmailAddress(Account.Id accountId) {
return accountId.get() + "@" + serverId;
}
@@ -198,21 +220,10 @@
}
String attentionSetUpdateToJson(AttentionSetUpdate attentionSetUpdate) {
- PersonIdent personIdent =
- new PersonIdent(
- getUsername(attentionSetUpdate.account()),
- getEmailAddress(attentionSetUpdate.account()));
StringBuilder stringBuilder = new StringBuilder();
- appendIdentString(stringBuilder, personIdent.getName(), personIdent.getEmailAddress());
+ appendAccountIdIdentString(stringBuilder, attentionSetUpdate.account());
return gson.toJson(
new AttentionStatusInNoteDb(
stringBuilder.toString(), attentionSetUpdate.operation(), attentionSetUpdate.reason()));
}
-
- static void appendIdentString(StringBuilder stringBuilder, String name, String emailAddress) {
- PersonIdent.appendSanitized(stringBuilder, name);
- stringBuilder.append(" <");
- PersonIdent.appendSanitized(stringBuilder, emailAddress);
- stringBuilder.append('>');
- }
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 0a71fa1..36a61cc0 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -207,9 +207,19 @@
public Stream<ChangeNotesResult> scan(Repository repo, Project.NameKey project)
throws IOException {
+ return scan(repo, project, null);
+ }
+
+ public Stream<ChangeNotesResult> scan(
+ Repository repo, Project.NameKey project, Predicate<Change.Id> changeIdPredicate)
+ throws IOException {
ScanResult sr = scanChangeIds(repo);
- return sr.all().stream().map(id -> scanOneChange(project, sr, id)).filter(Objects::nonNull);
+ Stream<Change.Id> idStream = sr.all().stream();
+ if (changeIdPredicate != null) {
+ idStream = idStream.filter(changeIdPredicate);
+ }
+ return idStream.map(id -> scanOneChange(project, sr, id)).filter(Objects::nonNull);
}
@Nullable
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 63f4e5d..41b1b94 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -602,7 +602,7 @@
if (assignee != null) {
if (assignee.isPresent()) {
addFooter(msg, FOOTER_ASSIGNEE);
- addIdent(msg, assignee.get()).append('\n');
+ noteUtil.appendAccountIdIdentString(msg, assignee.get()).append('\n');
} else {
addFooter(msg, FOOTER_ASSIGNEE).append('\n');
}
@@ -623,7 +623,7 @@
for (Map.Entry<Account.Id, ReviewerStateInternal> e : reviewers.entrySet()) {
addFooter(msg, e.getValue().getFooterKey());
- addIdent(msg, e.getKey()).append('\n');
+ noteUtil.appendAccountIdIdentString(msg, e.getKey()).append('\n');
}
for (Map.Entry<Address, ReviewerStateInternal> e : reviewersByEmail.entrySet()) {
@@ -640,7 +640,7 @@
}
Account.Id id = c.getColumnKey();
if (!id.equals(getAccountId())) {
- addIdent(msg.append(' '), id);
+ noteUtil.appendAccountIdIdentString(msg.append(' '), id);
}
msg.append('\n');
}
@@ -666,7 +666,7 @@
.append(label.label);
if (label.appliedBy != null) {
msg.append(": ");
- addIdent(msg, label.appliedBy);
+ noteUtil.appendAccountIdIdentString(msg, label.appliedBy);
}
msg.append('\n');
}
@@ -677,7 +677,7 @@
if (!Objects.equals(accountId, realAccountId)) {
addFooter(msg, FOOTER_REAL_USER);
- addIdent(msg, realAccountId).append('\n');
+ noteUtil.appendAccountIdIdentString(msg, realAccountId).append('\n');
}
if (isPrivate != null) {
@@ -799,10 +799,4 @@
private static boolean isIllegalTopic(String topic) {
return (topic != null && topic.contains("\""));
}
-
- private StringBuilder addIdent(StringBuilder sb, Account.Id accountId) {
- PersonIdent ident = noteUtil.newIdent(accountId, when, serverIdent);
- ChangeNoteUtil.appendIdentString(sb, ident.getName(), ident.getEmailAddress());
- return sb;
- }
}
diff --git a/java/com/google/gerrit/server/notedb/IntBlob.java b/java/com/google/gerrit/server/notedb/IntBlob.java
index 61b9ae0..ec5a83a 100644
--- a/java/com/google/gerrit/server/notedb/IntBlob.java
+++ b/java/com/google/gerrit/server/notedb/IntBlob.java
@@ -20,6 +20,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
+import com.google.common.flogger.FluentLogger;
import com.google.common.primitives.Ints;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Project;
@@ -42,6 +43,8 @@
/** An object blob in a Git repository that stores a single integer value. */
@AutoValue
public abstract class IntBlob {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
public static Optional<IntBlob> parse(Repository repo, String refName) throws IOException {
try (ObjectReader or = repo.newObjectReader()) {
return parse(repo, refName, or);
@@ -85,8 +88,13 @@
throws IOException {
ObjectId newId;
try (ObjectInserter ins = repo.newObjectInserter()) {
+ logger.atFine().log(
+ "storing value %d on %s in %s (oldId: %s)",
+ val, refName, projectName, oldId == null ? "null" : oldId.name());
newId = ins.insert(OBJ_BLOB, Integer.toString(val).getBytes(UTF_8));
ins.flush();
+ logger.atFine().log(
+ "successfully stored %d on %s as %s in %s", val, refName, newId.name(), projectName);
}
RefUpdate ru = repo.updateRef(refName);
if (oldId != null) {
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index 2d1a04a..ca97a1a 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -17,6 +17,7 @@
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.logging.TraceContext.newTimer;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
@@ -31,6 +32,8 @@
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.update.ChainedReceiveCommands;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -307,8 +310,15 @@
// we may have stale draft comments. Doing it in this order allows stale
// comments to be filtered out by ChangeNotes, reflecting the fact that
// comments can only go from DRAFT to PUBLISHED, not vice versa.
- BatchRefUpdate result = execute(changeRepo, dryrun, pushCert);
- execute(allUsersRepo, dryrun, null);
+ BatchRefUpdate result;
+ try (TraceContext.TraceTimer ignored =
+ newTimer("NoteDbUpdateManager#updateRepo", Metadata.empty())) {
+ result = execute(changeRepo, dryrun, pushCert);
+ }
+ try (TraceContext.TraceTimer ignored =
+ newTimer("NoteDbUpdateManager#updateAllUsersSync", Metadata.empty())) {
+ execute(allUsersRepo, dryrun, null);
+ }
if (!dryrun) {
// Only execute the asynchronous operation if we are not in dry-run mode: The dry run would
// have to run synchronous to be of any value at all. For the removal of draft comments from
diff --git a/java/com/google/gerrit/server/notedb/RepoSequence.java b/java/com/google/gerrit/server/notedb/RepoSequence.java
index 11ba8cd..47e12ff 100644
--- a/java/com/google/gerrit/server/notedb/RepoSequence.java
+++ b/java/com/google/gerrit/server/notedb/RepoSequence.java
@@ -30,6 +30,7 @@
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.Runnables;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
@@ -64,6 +65,8 @@
* numbers.
*/
public class RepoSequence {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
@FunctionalInterface
public interface Seed {
int get();
@@ -185,6 +188,7 @@
this.afterReadRef = requireNonNull(afterReadRef, "afterReadRef");
this.retryer = requireNonNull(retryer, "retryer");
+ logger.atFine().log("sequence batch size for %s is %s", name, batchSize);
counterLock = new ReentrantLock(true);
}
@@ -263,6 +267,7 @@
private void acquire(int count) {
try (Repository repo = repoManager.openRepository(projectName);
RevWalk rw = new RevWalk(repo)) {
+ logger.atFine().log("acquire %d ids on %s in %s", count, refName, projectName);
Optional<IntBlob> blob = IntBlob.parse(repo, refName, rw);
afterReadRef.run();
ObjectId oldId;
diff --git a/java/com/google/gerrit/server/notedb/Sequences.java b/java/com/google/gerrit/server/notedb/Sequences.java
index be68592..7a8e28f 100644
--- a/java/com/google/gerrit/server/notedb/Sequences.java
+++ b/java/com/google/gerrit/server/notedb/Sequences.java
@@ -32,6 +32,11 @@
@Singleton
public class Sequences {
+ private static final String SECTION_NOTEDB = "noteDb";
+ private static final String KEY_SEQUENCE_BATCH_SIZE = "sequenceBatchSize";
+ private static final int DEFAULT_ACCOUNTS_SEQUENCE_BATCH_SIZE = 1;
+ private static final int DEFAULT_CHANGES_SEQUENCE_BATCH_SIZE = 20;
+
public static final String NAME_ACCOUNTS = "accounts";
public static final String NAME_GROUPS = "groups";
public static final String NAME_CHANGES = "changes";
@@ -60,7 +65,12 @@
AllUsersName allUsers,
MetricMaker metrics) {
- int accountBatchSize = cfg.getInt("noteDb", "accounts", "sequenceBatchSize", 1);
+ int accountBatchSize =
+ cfg.getInt(
+ SECTION_NOTEDB,
+ NAME_ACCOUNTS,
+ KEY_SEQUENCE_BATCH_SIZE,
+ DEFAULT_ACCOUNTS_SEQUENCE_BATCH_SIZE);
accountSeq =
new RepoSequence(
repoManager,
@@ -70,7 +80,12 @@
() -> FIRST_ACCOUNT_ID,
accountBatchSize);
- int changeBatchSize = cfg.getInt("noteDb", "changes", "sequenceBatchSize", 20);
+ int changeBatchSize =
+ cfg.getInt(
+ SECTION_NOTEDB,
+ NAME_CHANGES,
+ KEY_SEQUENCE_BATCH_SIZE,
+ DEFAULT_CHANGES_SEQUENCE_BATCH_SIZE);
changeSeq =
new RepoSequence(
repoManager,
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index 4e1a30c..37de0d1 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -569,7 +569,12 @@
// even if the change is not part of the set of most recent changes that
// SearchingChangeCacheImpl returns.
Change.Id cId = Change.Id.fromRef(refName);
- requireNonNull(cId, () -> String.format("invalid change id for ref %s", refName));
+ if (cId == null) {
+ // The ref is not a valid change ref. Treat it as non-visible since it's not representing a
+ // change.
+ logger.atWarning().log("invalid change ref %s is not visible", refName);
+ return false;
+ }
ChangeNotes notes;
try {
notes = changeNotesFactory.create(projectState.getNameKey(), cId);
diff --git a/java/com/google/gerrit/server/project/ProjectJson.java b/java/com/google/gerrit/server/project/ProjectJson.java
index f2254d6..f00df53 100644
--- a/java/com/google/gerrit/server/project/ProjectJson.java
+++ b/java/com/google/gerrit/server/project/ProjectJson.java
@@ -18,6 +18,7 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelValue;
import com.google.gerrit.entities.Project;
@@ -34,6 +35,7 @@
/** Collection of routines to populate {@link ProjectInfo}. */
@Singleton
public class ProjectJson {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final AllProjectsName allProjects;
private final WebLinks webLinks;
@@ -50,7 +52,17 @@
for (LabelType t : projectState.getLabelTypes().getLabelTypes()) {
LabelTypeInfo labelInfo = new LabelTypeInfo();
labelInfo.values =
- t.getValues().stream().collect(toMap(LabelValue::formatValue, LabelValue::getText));
+ t.getValues().stream()
+ .collect(
+ toMap(
+ LabelValue::formatValue,
+ LabelValue::getText,
+ (v1, v2) -> {
+ logger.atSevere().log(
+ "Duplicate values for project: %s, label: %s found: '%s':'%s'",
+ projectState.getName(), t.getName(), v1, v2);
+ return v1;
+ }));
labelInfo.defaultValue = t.getDefaultValue();
info.labels.put(t.getName(), labelInfo);
}
diff --git a/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java b/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
index 54625a6..54915fb 100644
--- a/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
+++ b/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
@@ -26,9 +26,8 @@
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.annotations.Exports;
-import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.server.query.change.ChangeData;
-import com.google.inject.Inject;
+import com.google.inject.AbstractModule;
import com.google.inject.Singleton;
import java.util.ArrayList;
import java.util.Collection;
@@ -46,7 +45,7 @@
private static final String E_UNABLE_TO_FETCH_LABELS =
"Unable to fetch labels and approvals for the change";
- public static class Module extends FactoryModule {
+ public static class Module extends AbstractModule {
@Override
public void configure() {
bind(SubmitRule.class)
@@ -55,9 +54,6 @@
}
}
- @Inject
- IgnoreSelfApprovalRule() {}
-
@Override
public Optional<SubmitRecord> evaluate(ChangeData cd) {
List<LabelType> labelTypes;
diff --git a/java/com/google/gerrit/server/rules/PrologOptions.java b/java/com/google/gerrit/server/rules/PrologOptions.java
index da9b3ab..a176f04 100644
--- a/java/com/google/gerrit/server/rules/PrologOptions.java
+++ b/java/com/google/gerrit/server/rules/PrologOptions.java
@@ -15,18 +15,21 @@
package com.google.gerrit.server.rules;
import com.google.auto.value.AutoValue;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import java.util.Optional;
@AutoValue
public abstract class PrologOptions {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
public static PrologOptions defaultOptions() {
return new AutoValue_PrologOptions.Builder().logErrors(true).skipFilters(false).build();
}
public static PrologOptions dryRunOptions(String ruleToTest, boolean skipFilters) {
return new AutoValue_PrologOptions.Builder()
- .logErrors(false)
+ .logErrors(logger.atFine().isEnabled())
.skipFilters(skipFilters)
.rule(ruleToTest)
.build();
diff --git a/java/com/google/gerrit/server/schema/NoteDbSchemaVersionCheck.java b/java/com/google/gerrit/server/schema/NoteDbSchemaVersionCheck.java
index 33534fc..0360ec0 100644
--- a/java/com/google/gerrit/server/schema/NoteDbSchemaVersionCheck.java
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaVersionCheck.java
@@ -14,15 +14,20 @@
package com.google.gerrit.server.schema;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.ProvisionException;
+import org.eclipse.jgit.lib.Config;
public class NoteDbSchemaVersionCheck implements LifecycleListener {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
public static Module module() {
return new LifecycleModule() {
@Override
@@ -34,11 +39,16 @@
private final NoteDbSchemaVersionManager versionManager;
private final SitePaths sitePaths;
+ private Config gerritConfig;
@Inject
- NoteDbSchemaVersionCheck(NoteDbSchemaVersionManager versionManager, SitePaths sitePaths) {
+ NoteDbSchemaVersionCheck(
+ NoteDbSchemaVersionManager versionManager,
+ SitePaths sitePaths,
+ @GerritServerConfig Config gerritConfig) {
this.versionManager = versionManager;
this.sitePaths = sitePaths;
+ this.gerritConfig = gerritConfig;
}
@Override
@@ -53,7 +63,18 @@
sitePaths.site_path.toAbsolutePath()));
}
int expected = NoteDbSchemaVersions.LATEST;
- if (current != expected) {
+
+ if (current > expected
+ && gerritConfig.getBoolean("gerrit", "experimentalRollingUpgrade", false)) {
+ logger.atWarning().log(
+ "Gerrit has detected refs/meta/version %d different than the expected %d."
+ + "Bear in mind that this is supported ONLY for rolling upgrades to immediate next "
+ + "Gerrit version (e.g. v3.1 to v3.2). If this is not expected, remove gerrit.experimentalRollingUpgrade "
+ + "from $GERRIT_SITE/etc/gerrit.config and restart Gerrit."
+ + "Please note that gerrit.experimentalRollingUpgrade is intended to be used "
+ + "for the rolling upgrade phase only and should be disabled afterwards.",
+ current, expected);
+ } else if (current != expected) {
String advice =
current > expected
? "Downgrade is not supported"
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index 34e402e..5558c74 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -42,6 +42,7 @@
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
import com.google.gerrit.server.git.GroupCollector;
import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.notedb.ChangeNoteUtil;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectState;
@@ -398,7 +399,7 @@
Optional<Account> account =
args.accountCache.get(submitter.accountId()).map(AccountState::account);
if (account.isPresent() && account.get().fullName() != null) {
- return " by " + account.get().fullName();
+ return " by " + ChangeNoteUtil.getAccountIdAsUsername(account.get().id());
}
return "";
}
diff --git a/java/com/google/gerrit/server/validators/AccountActivationValidationListener.java b/java/com/google/gerrit/server/validators/AccountActivationValidationListener.java
index bc52308..9fdc9e6 100644
--- a/java/com/google/gerrit/server/validators/AccountActivationValidationListener.java
+++ b/java/com/google/gerrit/server/validators/AccountActivationValidationListener.java
@@ -26,6 +26,9 @@
/**
* Called when an account should be activated to allow validation of the account activation.
*
+ * <p>See {@link com.google.gerrit.extensions.events.AccountActivationListener} for a listener
+ * that's run after the account got activated.
+ *
* @param account the account that should be activated
* @throws ValidationException if validation fails
*/
@@ -34,6 +37,9 @@
/**
* Called when an account should be deactivated to allow validation of the account deactivation.
*
+ * <p>See {@link com.google.gerrit.extensions.events.AccountActivationListener} for a listener
+ * that's run after the account got deactivated.
+ *
* @param account the account that should be deactivated
* @throws ValidationException if validation fails
*/
diff --git a/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index 916775d..6997d96 100644
--- a/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -18,6 +18,7 @@
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.common.base.Splitter;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.BaseEncoding;
import com.google.gerrit.common.FileUtil;
@@ -38,6 +39,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.apache.sshd.common.SshException;
@@ -197,9 +199,15 @@
continue;
}
+ List<String> parts = Splitter.on(' ').splitToList(line);
+ if (parts.size() > 2) {
+ throw new IllegalArgumentException(
+ "Invalid peer key file format, only <key [comment]> lines supported");
+ }
try {
byte[] bin =
- BaseEncoding.base64().decode(new String(line.getBytes(ISO_8859_1), ISO_8859_1));
+ BaseEncoding.base64()
+ .decode(new String(parts.get(0).getBytes(ISO_8859_1), ISO_8859_1));
keys.add(new ByteArrayBuffer(bin).getRawPublicKey());
} catch (RuntimeException | SshException e) {
logBadKey(path, line, e);
diff --git a/java/com/google/gerrit/sshd/InactiveAccountDisconnector.java b/java/com/google/gerrit/sshd/InactiveAccountDisconnector.java
new file mode 100644
index 0000000..1086626
--- /dev/null
+++ b/java/com/google/gerrit/sshd/InactiveAccountDisconnector.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.events.AccountActivationListener;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.sshd.BaseCommand.Failure;
+import com.google.inject.Inject;
+import java.io.IOException;
+
+/** Closes open SSH connections upon account deactivation. */
+public class InactiveAccountDisconnector implements AccountActivationListener {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final SshDaemon sshDaemon;
+
+ @Inject
+ InactiveAccountDisconnector(SshDaemon sshDaemon) {
+ this.sshDaemon = sshDaemon;
+ }
+
+ @Override
+ public void onAccountDeactivated(int id) {
+ try {
+ SshUtil.forEachSshSession(
+ sshDaemon,
+ (sshId, sshSession, abstractSession, ioSession) -> {
+ CurrentUser sessionUser = sshSession.getUser();
+ if (sessionUser.isIdentifiedUser() && sessionUser.getAccountId().get() == id) {
+ logger.atInfo().log(
+ "Disconnecting SSH session %s because user %s(%d) got deactivated",
+ abstractSession, sessionUser.getLoggableName(), id);
+ try {
+ abstractSession.disconnect(-1, "user deactivated");
+ } catch (IOException e) {
+ logger.atWarning().withCause(e).log(
+ "Failure while deactivating session %s", abstractSession);
+ }
+ }
+ });
+ } catch (Failure e) {
+ // Ssh Daemon no longer running. Since we're only disconnecting connections anyways, this is
+ // most likely ok, so we log only at info level.
+ logger.atInfo().withCause(e).log("Failure while disconnecting deactivated account %d", id);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/sshd/SshModule.java b/java/com/google/gerrit/sshd/SshModule.java
index e4aa14c..9301f8a 100644
--- a/java/com/google/gerrit/sshd/SshModule.java
+++ b/java/com/google/gerrit/sshd/SshModule.java
@@ -19,6 +19,7 @@
import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
+import com.google.gerrit.extensions.events.AccountActivationListener;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.lifecycle.LifecycleModule;
@@ -103,6 +104,9 @@
DynamicItem.itemOf(binder(), SshCreateCommandInterceptor.class);
DynamicSet.setOf(binder(), SshExecuteCommandInterceptor.class);
+ DynamicSet.bind(binder(), AccountActivationListener.class)
+ .to(InactiveAccountDisconnector.class);
+
listener().toInstance(registerInParentInjectors());
listener().to(SshLog.class);
listener().to(SshDaemon.class);
diff --git a/java/com/google/gerrit/sshd/SshUtil.java b/java/com/google/gerrit/sshd/SshUtil.java
index eac9737..abbd81d 100644
--- a/java/com/google/gerrit/sshd/SshUtil.java
+++ b/java/com/google/gerrit/sshd/SshUtil.java
@@ -19,6 +19,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountSshKey;
+import com.google.gerrit.sshd.BaseCommand.Failure;
import com.google.gerrit.sshd.SshScope.Context;
import java.io.BufferedReader;
import java.io.IOException;
@@ -30,7 +31,10 @@
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.io.IoAcceptor;
+import org.apache.sshd.common.io.IoSession;
import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.session.helpers.AbstractSession;
import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
import org.apache.sshd.server.session.ServerSession;
@@ -152,4 +156,30 @@
final Account.Id account) {
return userFactory.create(sd.getRemoteAddress(), account);
}
+
+ public static void forEachSshSession(SshDaemon sshDaemon, SessionConsumer consumer)
+ throws Failure {
+ IoAcceptor ioAcceptor = sshDaemon.getIoAcceptor();
+ if (ioAcceptor == null) {
+ throw new Failure(1, "fatal: sshd no longer running");
+ }
+ ioAcceptor
+ .getManagedSessions()
+ .forEach(
+ (id, ioSession) -> {
+ AbstractSession abstractSession = AbstractSession.getSession(ioSession, true);
+ if (abstractSession != null) {
+ SshSession sshSession = abstractSession.getAttribute(SshSession.KEY);
+ if (sshSession != null) {
+ consumer.accept(id, sshSession, abstractSession, ioSession);
+ }
+ }
+ });
+ }
+
+ @FunctionalInterface
+ public interface SessionConsumer {
+ public void accept(
+ Long id, SshSession sshSession, AbstractSession abstractSession, IoSession ioSession);
+ }
}
diff --git a/java/com/google/gerrit/sshd/commands/CloseConnection.java b/java/com/google/gerrit/sshd/commands/CloseConnection.java
index 60a878a..093f647 100644
--- a/java/com/google/gerrit/sshd/commands/CloseConnection.java
+++ b/java/com/google/gerrit/sshd/commands/CloseConnection.java
@@ -23,15 +23,12 @@
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
import com.google.gerrit.sshd.SshDaemon;
-import com.google.gerrit.sshd.SshSession;
+import com.google.gerrit.sshd.SshUtil;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.sshd.common.future.CloseFuture;
-import org.apache.sshd.common.io.IoAcceptor;
-import org.apache.sshd.common.io.IoSession;
-import org.apache.sshd.common.session.helpers.AbstractSession;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
@@ -60,36 +57,26 @@
@Override
protected void run() throws Failure {
- IoAcceptor acceptor = sshDaemon.getIoAcceptor();
- if (acceptor == null) {
- throw new Failure(1, "fatal: sshd no longer running");
- }
- for (String sessionId : sessionIds) {
- boolean connectionFound = false;
- int id = (int) Long.parseLong(sessionId, 16);
- for (IoSession io : acceptor.getManagedSessions().values()) {
- AbstractSession serverSession = AbstractSession.getSession(io, true);
- SshSession sshSession =
- serverSession != null ? serverSession.getAttribute(SshSession.KEY) : null;
- if (sshSession != null && sshSession.getSessionId() == id) {
- connectionFound = true;
- stdout.println("closing connection " + sessionId + "...");
- CloseFuture future = io.close(true);
- if (wait) {
- try {
- future.await();
- stdout.println("closed connection " + sessionId);
- } catch (IOException e) {
- logger.atWarning().log(
- "Wait for connection to close interrupted: %s", e.getMessage());
+ SshUtil.forEachSshSession(
+ sshDaemon,
+ (k, sshSession, abstractSession, ioSession) -> {
+ String sessionId = String.format("%08x", sshSession.getSessionId());
+ if (sessionIds.remove(sessionId)) {
+ stdout.println("closing connection " + sessionId + "...");
+ CloseFuture future = ioSession.close(true);
+ if (wait) {
+ try {
+ future.await();
+ stdout.println("closed connection " + sessionId);
+ } catch (IOException e) {
+ logger.atWarning().log(
+ "Wait for connection to close interrupted: %s", e.getMessage());
+ }
}
}
- break;
- }
- }
- if (!connectionFound) {
- stderr.print("close connection " + sessionId + ": no such connection\n");
- }
+ });
+ for (String sessionId : sessionIds) {
+ stderr.print("close connection " + sessionId + ": no such connection\n");
}
}
}
diff --git a/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java b/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
index 7cb5377..8e08b1c 100644
--- a/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
@@ -26,6 +26,7 @@
import com.google.gerrit.mail.Address;
import com.google.gerrit.mail.EmailHeader;
import com.google.gerrit.testing.FakeEmailSender;
+import java.net.URL;
import org.eclipse.jgit.lib.Repository;
import org.junit.Test;
@@ -43,23 +44,25 @@
gApi.changes().id(result.getChangeId()).abandon();
assertThat(getMessageId(sender))
.isEqualTo(
- repository
- .getRefDatabase()
- .exactRef(result.getChange().getId().toRefPrefix() + "meta")
- .getObjectId()
- .getName()
- + "-HTML");
+ withPrefixAndSuffixForMessageId(
+ repository
+ .getRefDatabase()
+ .exactRef(result.getChange().getId().toRefPrefix() + "meta")
+ .getObjectId()
+ .getName()
+ + "-HTML"));
sender.clear();
gApi.changes().id(result.getChangeId()).restore();
assertThat(getMessageId(sender))
.isEqualTo(
- repository
- .getRefDatabase()
- .exactRef(result.getChange().getId().toRefPrefix() + "meta")
- .getObjectId()
- .getName()
- + "-HTML");
+ withPrefixAndSuffixForMessageId(
+ repository
+ .getRefDatabase()
+ .exactRef(result.getChange().getId().toRefPrefix() + "meta")
+ .getObjectId()
+ .getName()
+ + "-HTML"));
}
@Test
@@ -80,19 +83,22 @@
assertThat(getMessageId(sender))
.isEqualTo(
- allUsersRepo
- .getRefDatabase()
- .exactRef(RefNames.refsUsers(admin.id()))
- .getObjectId()
- .getName()
- + "-HTML");
+ withPrefixAndSuffixForMessageId(
+ allUsersRepo
+ .getRefDatabase()
+ .exactRef(RefNames.refsUsers(admin.id()))
+ .getObjectId()
+ .getName()
+ + "-HTML"));
}
@Test
public void messageIdHeaderFromPasswordUpdate() throws Exception {
sender.clear();
String newPassword = gApi.accounts().self().generateHttpPassword();
- assertThat(getMessageId(sender)).contains("HTTP password change-" + admin.id().toString());
+ assertThat(newPassword).isNotNull();
+ assertThat(getMessageId(sender))
+ .containsMatch("<HTTP_password_change-" + admin.id().toString() + ".*@.*>");
}
@Test
@@ -123,4 +129,10 @@
(Iterables.getOnlyElement(sender.getMessages()).headers().get("Message-ID")))
.getString();
}
+
+ // Each message-id must start with '<' and end with '>'. Also, it must contain no spaces and it
+ // must contain a '@'.
+ private String withPrefixAndSuffixForMessageId(String id) throws Exception {
+ return "<" + id + "@" + new URL(canonicalWebUrl.get()).getHost() + ">";
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index d5dd241..f9ba8a2 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -45,6 +45,10 @@
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
import com.github.rholder.retry.StopStrategies;
import com.google.common.collect.FluentIterable;
@@ -84,6 +88,7 @@
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.api.accounts.AccountApi;
import com.google.gerrit.extensions.api.accounts.AccountInput;
import com.google.gerrit.extensions.api.accounts.DeleteDraftCommentsInput;
import com.google.gerrit.extensions.api.accounts.DeletedDraftCommentInfo;
@@ -104,6 +109,7 @@
import com.google.gerrit.extensions.common.GpgKeyInfo;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.common.SshKeyInfo;
+import com.google.gerrit.extensions.events.AccountActivationListener;
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -115,6 +121,7 @@
import com.google.gerrit.gpg.Fingerprint;
import com.google.gerrit.gpg.PublicKeyStore;
import com.google.gerrit.gpg.testing.TestKey;
+import com.google.gerrit.httpd.CacheBasedWebSession;
import com.google.gerrit.mail.Address;
import com.google.gerrit.server.ExceptionHook;
import com.google.gerrit.server.ServerInitiated;
@@ -159,6 +166,14 @@
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.BasicCookieStore;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
@@ -183,6 +198,7 @@
import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.junit.After;
+import org.junit.Before;
import org.junit.Test;
public class AccountIT extends AbstractDaemonTest {
@@ -221,6 +237,9 @@
@Inject protected GroupOperations groupOperations;
+ private BasicCookieStore httpCookieStore;
+ private CloseableHttpClient httpclient;
+
@After
public void clearPublicKeyStore() throws Exception {
try (Repository repo = repoManager.openRepository(allUsers)) {
@@ -247,6 +266,16 @@
}
}
+ @Before
+ public void createHttpClient() {
+ httpCookieStore = new BasicCookieStore();
+ httpclient =
+ HttpClientBuilder.create()
+ .disableRedirectHandling()
+ .setDefaultCookieStore(httpCookieStore)
+ .build();
+ }
+
protected void assertLabelPermission(
Project.NameKey project,
GroupReference groupReference,
@@ -577,13 +606,50 @@
}
@Test
+ @GerritConfig(name = "auth.type", value = "DEVELOPMENT_BECOME_ANY_ACCOUNT")
+ public void activeUserGetSessionCookieOnLogin() throws Exception {
+ Integer accountId = accountIdApi().get()._accountId;
+ assertThat(accountIdApi().getActive()).isTrue();
+
+ webLogin(accountId);
+ assertThat(getCookiesNames()).contains(CacheBasedWebSession.ACCOUNT_COOKIE);
+ }
+
+ @Test
+ @GerritConfig(name = "auth.type", value = "DEVELOPMENT_BECOME_ANY_ACCOUNT")
+ public void inactiveUserDoesNotGetCookieOnLogin() throws Exception {
+ Integer accountId = accountIdApi().get()._accountId;
+ accountIdApi().setActive(false);
+ assertThat(accountIdApi().getActive()).isFalse();
+
+ webLogin(accountId);
+ assertThat(getCookiesNames()).isEmpty();
+ }
+
+ @Test
+ @GerritConfig(name = "auth.type", value = "DEVELOPMENT_BECOME_ANY_ACCOUNT")
+ public void userDeactivatedAfterLoginDoesNotGetCookie() throws Exception {
+ Integer accountId = accountIdApi().get()._accountId;
+ assertThat(accountIdApi().getActive()).isTrue();
+
+ webLogin(accountId);
+ assertThat(getCookiesNames()).contains(CacheBasedWebSession.ACCOUNT_COOKIE);
+ httpGetAndAssertStatus("accounts/self/detail", HttpServletResponse.SC_OK);
+
+ accountIdApi().setActive(false);
+ assertThat(accountIdApi().getActive()).isFalse();
+
+ httpGetAndAssertStatus("accounts/self/detail", HttpServletResponse.SC_FORBIDDEN);
+ }
+
+ @Test
public void validateAccountActivation() throws Exception {
Account.Id activatableAccountId =
accountOperations.newAccount().inactive().preferredEmail("foo@activatable.com").create();
Account.Id deactivatableAccountId =
accountOperations.newAccount().preferredEmail("foo@deactivatable.com").create();
- AccountActivationValidationListener listener =
+ AccountActivationValidationListener validationListener =
new AccountActivationValidationListener() {
@Override
public void validateActivation(AccountState account) throws ValidationException {
@@ -601,7 +667,11 @@
}
}
};
- try (Registration registration = extensionRegistry.newRegistration().add(listener)) {
+
+ AccountActivationListener listener = mock(AccountActivationListener.class);
+
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(validationListener).add(listener)) {
/* Test account that can be activated, but not deactivated */
// Deactivate account that is already inactive
ResourceConflictException thrown =
@@ -610,14 +680,18 @@
() -> gApi.accounts().id(activatableAccountId.get()).setActive(false));
assertThat(thrown).hasMessageThat().isEqualTo("account not active");
assertThat(accountOperations.account(activatableAccountId).get().active()).isFalse();
+ verifyZeroInteractions(listener);
// Activate account that can be activated
gApi.accounts().id(activatableAccountId.get()).setActive(true);
assertThat(accountOperations.account(activatableAccountId).get().active()).isTrue();
+ verify(listener).onAccountActivated(activatableAccountId.get());
+ verifyNoMoreInteractions(listener);
// Activate account that is already active
gApi.accounts().id(activatableAccountId.get()).setActive(true);
assertThat(accountOperations.account(activatableAccountId).get().active()).isTrue();
+ verifyZeroInteractions(listener);
// Try deactivating account that cannot be deactivated
thrown =
@@ -626,15 +700,19 @@
() -> gApi.accounts().id(activatableAccountId.get()).setActive(false));
assertThat(thrown).hasMessageThat().isEqualTo("not allowed to deactive account");
assertThat(accountOperations.account(activatableAccountId).get().active()).isTrue();
+ verifyZeroInteractions(listener);
/* Test account that can be deactivated, but not activated */
// Activate account that is already inactive
gApi.accounts().id(deactivatableAccountId.get()).setActive(true);
assertThat(accountOperations.account(deactivatableAccountId).get().active()).isTrue();
+ verifyZeroInteractions(listener);
// Deactivate account that can be deactivated
gApi.accounts().id(deactivatableAccountId.get()).setActive(false);
assertThat(accountOperations.account(deactivatableAccountId).get().active()).isFalse();
+ verify(listener).onAccountDeactivated(deactivatableAccountId.get());
+ verifyNoMoreInteractions(listener);
// Deactivate account that is already inactive
thrown =
@@ -643,6 +721,7 @@
() -> gApi.accounts().id(deactivatableAccountId.get()).setActive(false));
assertThat(thrown).hasMessageThat().isEqualTo("account not active");
assertThat(accountOperations.account(deactivatableAccountId).get().active()).isFalse();
+ verifyZeroInteractions(listener);
// Try activating account that cannot be activated
thrown =
@@ -651,6 +730,7 @@
() -> gApi.accounts().id(deactivatableAccountId.get()).setActive(true));
assertThat(thrown).hasMessageThat().isEqualTo("not allowed to active account");
assertThat(accountOperations.account(deactivatableAccountId).get().active()).isFalse();
+ verifyZeroInteractions(listener);
}
}
@@ -2981,6 +3061,30 @@
assertThat(Iterables.getOnlyElement(accounts)).isEqualTo(expectedAccount.id());
}
+ private AccountApi accountIdApi() throws RestApiException {
+ return gApi.accounts().id(user.id().get());
+ }
+
+ private Set<String> getCookiesNames() {
+ Set<String> cookieNames =
+ httpCookieStore.getCookies().stream()
+ .map(cookie -> cookie.getName())
+ .collect(Collectors.toSet());
+ return cookieNames;
+ }
+
+ private void webLogin(Integer accountId) throws IOException, ClientProtocolException {
+ httpGetAndAssertStatus(
+ "login?account_id=" + accountId, HttpServletResponse.SC_MOVED_TEMPORARILY);
+ }
+
+ private void httpGetAndAssertStatus(String urlPath, int expectedHttpStatus)
+ throws ClientProtocolException, IOException {
+ HttpGet httpGet = new HttpGet(canonicalWebUrl.get() + urlPath);
+ HttpResponse loginResponse = httpclient.execute(httpGet);
+ assertThat(loginResponse.getStatusLine().getStatusCode()).isEqualTo(expectedHttpStatus);
+ }
+
private static class RefUpdateCounter implements GitReferenceUpdatedListener {
private final AtomicLongMap<String> countsByProjectRefs = AtomicLongMap.create();
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountListenersIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountListenersIT.java
new file mode 100644
index 0000000..80eff96
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountListenersIT.java
@@ -0,0 +1,212 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.accounts;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.extensions.events.AccountActivationListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.validators.AccountActivationValidationListener;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests the wiring of a real plugin's account listeners
+ *
+ * <p>This test really puts focus on the wiring of the account listeners. Tests for the inner
+ * workings of account activation/deactivation can be found in {@link AccountIT}.
+ */
+@TestPlugin(
+ name = "account-listener-it-plugin",
+ sysModule = "com.google.gerrit.acceptance.api.accounts.AccountListenersIT$Module")
+public class AccountListenersIT extends LightweightPluginDaemonTest {
+ @Inject private AccountOperations accountOperations;
+
+ public static class Module extends AbstractModule {
+ @Override
+ protected void configure() {
+ DynamicSet.bind(binder(), AccountActivationValidationListener.class).to(Validator.class);
+ DynamicSet.bind(binder(), AccountActivationListener.class).to(Listener.class);
+ }
+ }
+
+ Validator validator;
+ Listener listener;
+
+ @Before
+ public void setUp() {
+ validator = plugin.getSysInjector().getInstance(Validator.class);
+
+ listener = plugin.getSysInjector().getInstance(Listener.class);
+ }
+
+ @Test
+ public void testActivation() throws RestApiException {
+ int id = accountOperations.newAccount().inactive().create().get();
+
+ gApi.accounts().id(id).setActive(true);
+
+ validator.assertActivationValidation(id);
+ listener.assertActivated(id);
+ assertNoMoreEvents();
+ assertThat(gApi.accounts().id(id).getActive()).isTrue();
+ }
+
+ @Test
+ public void testActivationProhibited() throws RestApiException {
+ int id = accountOperations.newAccount().inactive().create().get();
+
+ validator.failActivationValidations();
+
+ assertThrows(
+ ResourceConflictException.class,
+ () -> {
+ gApi.accounts().id(id).setActive(true);
+ });
+
+ validator.assertActivationValidation(id);
+ // No call to activation listener as validation failed
+ assertNoMoreEvents();
+ assertThat(gApi.accounts().id(id).getActive()).isFalse();
+ }
+
+ @Test
+ public void testDeactivation() throws RestApiException {
+ int id = accountOperations.newAccount().active().create().get();
+
+ gApi.accounts().id(id).setActive(false);
+
+ validator.assertDeactivationValidation(id);
+ listener.assertDeactivated(id);
+ assertNoMoreEvents();
+ assertThat(gApi.accounts().id(id).getActive()).isFalse();
+ }
+
+ @Test
+ public void testDeactivationProhibited() throws RestApiException {
+ int id = accountOperations.newAccount().active().create().get();
+
+ validator.failDeactivationValidations();
+
+ assertThrows(
+ ResourceConflictException.class,
+ () -> {
+ gApi.accounts().id(id).setActive(false);
+ });
+
+ validator.assertDeactivationValidation(id);
+ // No call to activation listener as validation failed
+ assertNoMoreEvents();
+ assertThat(gApi.accounts().id(id).getActive()).isTrue();
+ }
+
+ private void assertNoMoreEvents() {
+ validator.assertNoMoreEvents();
+ listener.assertNoMoreEvents();
+ }
+
+ @Singleton
+ public static class Validator implements AccountActivationValidationListener {
+ private Integer lastIdActivationValidation;
+ private Integer lastIdDeactivationValidation;
+ private boolean failActivationValidations;
+ private boolean failDeactivationValidations;
+
+ @Override
+ public void validateActivation(AccountState account) throws ValidationException {
+ assertThat(lastIdActivationValidation).isNull();
+ lastIdActivationValidation = account.account().id().get();
+ if (failActivationValidations) {
+ throw new ValidationException("testing validation failure");
+ }
+ }
+
+ @Override
+ public void validateDeactivation(AccountState account) throws ValidationException {
+ assertThat(lastIdDeactivationValidation).isNull();
+ lastIdDeactivationValidation = account.account().id().get();
+ if (failDeactivationValidations) {
+ throw new ValidationException("testing validation failure");
+ }
+ }
+
+ public void failActivationValidations() {
+ failActivationValidations = true;
+ }
+
+ public void failDeactivationValidations() {
+ failDeactivationValidations = true;
+ }
+
+ private void assertNoMoreEvents() {
+ assertThat(lastIdActivationValidation).isNull();
+ assertThat(lastIdDeactivationValidation).isNull();
+ }
+
+ private void assertActivationValidation(int id) {
+ assertThat(lastIdActivationValidation).isEqualTo(id);
+ lastIdActivationValidation = null;
+ }
+
+ private void assertDeactivationValidation(int id) {
+ assertThat(lastIdDeactivationValidation).isEqualTo(id);
+ lastIdDeactivationValidation = null;
+ }
+ }
+
+ @Singleton
+ public static class Listener implements AccountActivationListener {
+ private Integer lastIdActivated;
+ private Integer lastIdDeactivated;
+
+ @Override
+ public void onAccountActivated(int id) {
+ assertThat(lastIdActivated).isNull();
+ lastIdActivated = id;
+ }
+
+ @Override
+ public void onAccountDeactivated(int id) {
+ assertThat(lastIdDeactivated).isNull();
+ lastIdDeactivated = id;
+ }
+
+ private void assertNoMoreEvents() {
+ assertThat(lastIdActivated).isNull();
+ assertThat(lastIdDeactivated).isNull();
+ }
+
+ private void assertDeactivated(int id) {
+ assertThat(lastIdDeactivated).isEqualTo(id);
+ lastIdDeactivated = null;
+ }
+
+ private void assertActivated(int id) {
+ assertThat(lastIdActivated).isEqualTo(id);
+ lastIdActivated = null;
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
index 7d97934..11ca391 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
@@ -25,7 +25,6 @@
import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
-import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.common.data.ContributorAgreement;
@@ -64,7 +63,6 @@
private ContributorAgreement caNoAutoVerify;
@Inject private GroupOperations groupOperations;
@Inject private RequestScopeOperations requestScopeOperations;
- @Inject private ProjectOperations projectOperations;
protected void setUseContributorAgreements(InheritableBoolean value) throws Exception {
try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java
index 3228900..a8fd834 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java
@@ -24,7 +24,6 @@
import com.google.gerrit.mail.MailMessage;
import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.util.time.TimeUtil;
-import java.sql.Timestamp;
import java.time.Instant;
import javax.inject.Inject;
import org.eclipse.jgit.lib.Repository;
@@ -38,7 +37,7 @@
try (Repository repo = repoManager.openRepository(allUsers)) {
String messageId = messageIdGenerator.fromAccountUpdate(admin.id()).id();
String sha1 =
- repo.getRefDatabase().getRef(RefNames.refsUsers(admin.id())).getObjectId().getName();
+ repo.getRefDatabase().findRef(RefNames.refsUsers(admin.id())).getObjectId().getName();
assertThat(sha1).isEqualTo(messageId);
}
}
@@ -51,7 +50,7 @@
String messageId = messageIdGenerator.fromChangeUpdate(project, patchsetId).id();
String sha1 =
repo.getRefDatabase()
- .getRef(String.format("%smeta", patchsetId.changeId().toRefPrefix()))
+ .findRef(String.format("%smeta", patchsetId.changeId().toRefPrefix()))
.getObjectId()
.getName();
assertThat(sha1).isEqualTo(messageId);
@@ -74,7 +73,7 @@
@Test
public void fromReasonAccountIdAndTimestamp() throws Exception {
String reason = "reason";
- Timestamp timestamp = TimeUtil.nowTs();
+ Instant timestamp = TimeUtil.now();
assertThat(
messageIdGenerator.fromReasonAccountIdAndTimestamp(reason, admin.id(), timestamp).id())
.isEqualTo(reason + "-" + admin.id().toString() + "-" + timestamp.toString());
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 82a19d4..744035e 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -3065,7 +3065,8 @@
assertThat(commitPatchSetCreation.getShortMessage()).isEqualTo("Create patch set 2");
PersonIdent expectedAuthor =
- changeNoteUtil.newIdent(getAccount(admin.id()).id(), c.updated, serverIdent.get());
+ changeNoteUtil.newAccountIdIdent(
+ getAccount(admin.id()).id(), c.updated, serverIdent.get());
assertThat(commitPatchSetCreation.getAuthorIdent()).isEqualTo(expectedAuthor);
assertThat(commitPatchSetCreation.getCommitterIdent())
.isEqualTo(new PersonIdent(serverIdent.get(), c.updated));
@@ -3074,7 +3075,8 @@
RevCommit commitChangeCreation = rw.parseCommit(commitPatchSetCreation.getParent(0));
assertThat(commitChangeCreation.getShortMessage()).isEqualTo("Create change");
expectedAuthor =
- changeNoteUtil.newIdent(getAccount(admin.id()).id(), c.created, serverIdent.get());
+ changeNoteUtil.newAccountIdIdent(
+ getAccount(admin.id()).id(), c.created, serverIdent.get());
assertThat(commitChangeCreation.getAuthorIdent()).isEqualTo(expectedAuthor);
assertThat(commitChangeCreation.getCommitterIdent())
.isEqualTo(new PersonIdent(serverIdent.get(), c.created));
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index 1083377..14288b5 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -1071,7 +1071,7 @@
PersonIdent committer = serverIdent.get();
PersonIdent author =
- noteUtil.newIdent(getAccount(admin.id()).id(), committer.getWhen(), committer);
+ noteUtil.newAccountIdIdent(getAccount(admin.id()).id(), committer.getWhen(), committer);
tr.branch(RefNames.changeMetaRef(cd3.getId()))
.commit()
.author(author)
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
index 38b7e0e..cf349ab 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
@@ -21,7 +21,6 @@
import com.google.gerrit.testing.ConfigSuite;
import com.google.inject.Injector;
import org.eclipse.jgit.lib.Config;
-import org.junit.Before;
public class ElasticReindexIT extends AbstractReindexTests {
@@ -39,10 +38,4 @@
public void configureIndex(Injector injector) {
createAllIndexes(injector);
}
-
- @Before
- public void reindexFirstSinceElastic() throws Exception {
- assertServerStartupFails();
- runGerrit("reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace");
- }
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 2779284..2901361 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -1348,7 +1348,7 @@
} else if (getSubmitType() == SubmitType.REBASE_ALWAYS) {
assertThat(last).startsWith("Change has been successfully rebased and submitted as");
} else {
- assertThat(last).isEqualTo("Change has been successfully merged by Administrator");
+ assertThat(last).isEqualTo("Change has been successfully merged by Gerrit User 1000000");
}
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index a0ebf02..68e9b14 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -429,7 +429,8 @@
assertThat(commit.getShortMessage()).isEqualTo("Create change");
PersonIdent expectedAuthor =
- changeNoteUtil.newIdent(getAccount(admin.id()).id(), c.created, serverIdent.get());
+ changeNoteUtil.newAccountIdIdent(
+ getAccount(admin.id()).id(), c.created, serverIdent.get());
assertThat(commit.getAuthorIdent()).isEqualTo(expectedAuthor);
assertThat(commit.getCommitterIdent())
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
index 0e23603..0514e03 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -149,11 +149,8 @@
@Test
public void grantRevertPermissionDoesntDeleteAdminsPreferences() throws Exception {
- String refsHeads = "refs/heads/*";
GroupReference registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS);
GroupReference otherGroup = systemGroupBackend.getGroup(ANONYMOUS_USERS);
- String groupId = "global:Registered-Users";
- String otherGroupId = "global:Anonymous-Users";
try (Repository repo = repoManager.openRepository(newProjectName)) {
MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, newProjectName, repo);
diff --git a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index 069387c..e39f967 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -831,7 +831,7 @@
private void addNoteDbCommit(Change.Id id, String commitMessage) throws Exception {
PersonIdent committer = serverIdent.get();
PersonIdent author =
- noteUtil.newIdent(getAccount(admin.id()).id(), committer.getWhen(), committer);
+ noteUtil.newAccountIdIdent(getAccount(admin.id()).id(), committer.getWhen(), committer);
serverSideTestRepo
.branch(RefNames.changeMetaRef(id))
.commit()
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index 4ae77da..d7d67b8 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -48,6 +48,7 @@
import com.google.gerrit.testing.TestCommentHelper;
import com.google.inject.Inject;
import com.google.inject.Module;
+import java.net.URL;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Collection;
@@ -300,7 +301,7 @@
// ensure the message header contains a valid message id.
assertThat(((EmailHeader.String) (message.headers().get("Message-ID"))).getString())
- .isEqualTo("some id-REJECTION-HTML");
+ .containsMatch("<someid-REJECTION-HTML@" + new URL(canonicalWebUrl.get()).getHost() + ">");
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
index 5421d8c..bbe7b81 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
@@ -18,6 +18,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
@@ -29,6 +30,9 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Spliterator;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
import org.junit.Test;
@NoHttpd
@@ -165,6 +169,38 @@
assertThat(commands).containsExactlyElementsIn(SLAVE_COMMANDS.get("gerrit")).inOrder();
}
+ @Test
+ @Sandboxed
+ public void showConnections() throws Exception {
+ Spliterator<String> connectionsOutput =
+ getOutputLines(adminSshSession.exec("gerrit show-connections"));
+
+ assertThat(findConnectionsInOutput(connectionsOutput, "user")).hasSize(1);
+ }
+
+ @Test
+ @Sandboxed
+ public void cloeConnections() throws Exception {
+ List<String> connectionsOutput =
+ findConnectionsInOutput(
+ getOutputLines(adminSshSession.exec("gerrit show-connections")), "user");
+ String connectionId =
+ Splitter.on(" ")
+ .trimResults()
+ .omitEmptyStrings()
+ .split(connectionsOutput.get(0))
+ .iterator()
+ .next();
+
+ String closeConnectionOutput = adminSshSession.exec("gerrit close-connection " + connectionId);
+ assertThat(closeConnectionOutput).contains(connectionId);
+
+ assertThat(
+ findConnectionsInOutput(
+ getOutputLines(adminSshSession.exec("gerrit show-connections")), "user"))
+ .isEmpty();
+ }
+
private List<String> parseCommandsFromGerritHelpText(String helpText) {
List<String> commands = new ArrayList<>();
@@ -197,4 +233,16 @@
return commands;
}
+
+ private List<String> findConnectionsInOutput(Spliterator<String> connectionsOutput, String user) {
+ List<String> connections =
+ StreamSupport.stream(connectionsOutput, false)
+ .filter(s -> s.contains("localhost") && s.contains(user))
+ .collect(Collectors.toList());
+ return connections;
+ }
+
+ private Spliterator<String> getOutputLines(String output) throws Exception {
+ return Splitter.on("\n").trimResults().omitEmptyStrings().split(output).spliterator();
+ }
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
index 15094fd..c20650f 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -43,7 +43,7 @@
case V6_7:
return "blacktop/elasticsearch:6.7.2";
case V6_8:
- return "blacktop/elasticsearch:6.8.9";
+ return "blacktop/elasticsearch:6.8.10";
case V7_0:
return "blacktop/elasticsearch:7.0.1";
case V7_1:
@@ -59,7 +59,7 @@
case V7_6:
return "blacktop/elasticsearch:7.6.2";
case V7_7:
- return "blacktop/elasticsearch:7.7.0";
+ return "blacktop/elasticsearch:7.7.1";
}
throw new IllegalStateException("No tests for version: " + version.name());
}
diff --git a/javatests/com/google/gerrit/httpd/AllowRenderInFrameFilterTest.java b/javatests/com/google/gerrit/httpd/AllowRenderInFrameFilterTest.java
new file mode 100644
index 0000000..2679829
--- /dev/null
+++ b/javatests/com/google/gerrit/httpd/AllowRenderInFrameFilterTest.java
@@ -0,0 +1,136 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.httpd.AllowRenderInFrameFilter.X_FRAME_OPTIONS_HEADER_NAME;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import com.google.gerrit.httpd.AllowRenderInFrameFilter.XFrameOption;
+import java.io.IOException;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class AllowRenderInFrameFilterTest {
+
+ private Config cfg = new Config();
+ @Mock ServletRequest request;
+ @Mock HttpServletResponse response;
+ @Mock FilterChain filterChain;
+
+ @Test
+ public void shouldDenyInFrameRenderingWhenCanRenderInFrameIsFalse()
+ throws IOException, ServletException {
+ cfg.setBoolean("gerrit", null, "canLoadInIFrame", false);
+
+ AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
+ objectUnderTest.doFilter(request, response, filterChain);
+
+ verify(response, times(1)).addHeader(X_FRAME_OPTIONS_HEADER_NAME, "DENY");
+ }
+
+ @Test
+ public void shouldDenyInFrameRenderingWhenCanRenderInFrameIsFalseAndXFormOptionIsSAMEORIGIN()
+ throws IOException, ServletException {
+ cfg.setBoolean("gerrit", null, "canLoadInIFrame", false);
+ cfg.setEnum("gerrit", null, "xframeOption", XFrameOption.SAMEORIGIN);
+
+ AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
+ objectUnderTest.doFilter(request, response, filterChain);
+
+ verify(response, times(1)).addHeader(X_FRAME_OPTIONS_HEADER_NAME, "DENY");
+ }
+
+ @Test
+ public void shouldDenyInFrameRenderingWhenCanRenderInFrameIsFalseAndXFormOptionIsALLOW()
+ throws IOException, ServletException {
+ cfg.setBoolean("gerrit", null, "canLoadInIFrame", false);
+ cfg.setEnum("gerrit", null, "xframeOption", XFrameOption.ALLOW);
+
+ AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
+ objectUnderTest.doFilter(request, response, filterChain);
+
+ verify(response, times(1)).addHeader(X_FRAME_OPTIONS_HEADER_NAME, "DENY");
+ }
+
+ @Test
+ public void shouldRestrictAccessToSAMEORIGINWhenCanRenderInFrameIsTrue()
+ throws IOException, ServletException {
+ cfg.setBoolean("gerrit", null, "canLoadInIFrame", true);
+
+ AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
+ objectUnderTest.doFilter(request, response, filterChain);
+
+ verify(response, times(1)).addHeader(X_FRAME_OPTIONS_HEADER_NAME, "SAMEORIGIN");
+ }
+
+ @Test
+ public void shouldSkipHeaderWhenCanRenderInFrameIsTrueAndXFormOptionIsALLOW()
+ throws IOException, ServletException {
+ cfg.setBoolean("gerrit", null, "canLoadInIFrame", true);
+ cfg.setEnum("gerrit", null, "xframeOption", XFrameOption.ALLOW);
+
+ AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
+ objectUnderTest.doFilter(request, response, filterChain);
+
+ verify(response, never()).addHeader(eq(X_FRAME_OPTIONS_HEADER_NAME), anyString());
+ }
+
+ @Test
+ public void shouldRestrictAccessToSAMEORIGINWhenCanRenderInFrameIsTrueAndXFormOptionIsSAMEORIGIN()
+ throws IOException, ServletException {
+ cfg.setBoolean("gerrit", null, "canLoadInIFrame", true);
+ cfg.setEnum("gerrit", null, "xframeOption", XFrameOption.SAMEORIGIN);
+
+ AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
+ objectUnderTest.doFilter(request, response, filterChain);
+
+ verify(response, times(1)).addHeader(X_FRAME_OPTIONS_HEADER_NAME, "SAMEORIGIN");
+ }
+
+ @Test
+ public void shouldIgnoreXFrameOriginCaseSensitivity() throws IOException, ServletException {
+ cfg.setBoolean("gerrit", null, "canLoadInIFrame", true);
+ cfg.setString("gerrit", null, "xframeOption", "sameOrigin");
+
+ AllowRenderInFrameFilter objectUnderTest = new AllowRenderInFrameFilter(cfg);
+ objectUnderTest.doFilter(request, response, filterChain);
+
+ verify(response, times(1)).addHeader(X_FRAME_OPTIONS_HEADER_NAME, "SAMEORIGIN");
+ }
+
+ @Test
+ public void shouldThrowExceptionWhenUnknownXFormOptionValue() {
+ cfg.setBoolean("gerrit", null, "canLoadInIFrame", true);
+ cfg.setString("gerrit", null, "xframeOption", "unsupported value");
+
+ IllegalArgumentException e =
+ assertThrows(IllegalArgumentException.class, () -> new AllowRenderInFrameFilter(cfg));
+ assertThat(e).hasMessageThat().contains("gerrit.xframeOption=unsupported value");
+ }
+}
diff --git a/javatests/com/google/gerrit/integration/ssh/BUILD b/javatests/com/google/gerrit/integration/ssh/BUILD
new file mode 100644
index 0000000..dc8e68c
--- /dev/null
+++ b/javatests/com/google/gerrit/integration/ssh/BUILD
@@ -0,0 +1,7 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+ srcs = ["PeerKeysAuthIT.java"],
+ group = "peer-keys-auth",
+ labels = ["ssh"],
+)
diff --git a/javatests/com/google/gerrit/integration/ssh/PeerKeysAuthIT.java b/javatests/com/google/gerrit/integration/ssh/PeerKeysAuthIT.java
new file mode 100644
index 0000000..a219cc2
--- /dev/null
+++ b/javatests/com/google/gerrit/integration/ssh/PeerKeysAuthIT.java
@@ -0,0 +1,104 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.integration.ssh;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.GerritServer.TestSshServerAddress;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.StandaloneSiteTest;
+import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.common.Version;
+import com.google.gerrit.server.PeerDaemonUser;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.file.Files;
+import org.junit.Test;
+
+@NoHttpd
+@UseSsh
+public class PeerKeysAuthIT extends StandaloneSiteTest {
+ private static final String[] SSH_KEYGEN_CMD =
+ new String[] {"ssh-keygen", "-t", "rsa", "-q", "-P", "", "-f", "id_rsa"};
+ private static final String[] SSH_COMMAND =
+ new String[] {
+ "ssh",
+ "-o",
+ "UserKnownHostsFile=/dev/null",
+ "-o",
+ "StrictHostKeyChecking=no",
+ "-o",
+ "IdentitiesOnly=yes",
+ "-i"
+ };
+
+ @Inject private @TestSshServerAddress InetSocketAddress sshAddress;
+
+ @Test
+ public void test() throws Exception {
+ try (ServerContext ctx = startServer()) {
+ ctx.getInjector().injectMembers(this);
+ // Generate private/public key for user
+ execute(ImmutableList.<String>builder().add(SSH_KEYGEN_CMD).build());
+
+ String[] parts =
+ new String(Files.readAllBytes(sitePaths.data_dir.resolve("id_rsa.pub")), UTF_8)
+ .split(" ");
+
+ // Loose algorithm at index 0, verify the format: "key comment"
+ Files.write(
+ sitePaths.peer_keys, String.format("%s %s", parts[1], parts[2]).getBytes(ISO_8859_1));
+ assertContent(execGerritVersionCommand());
+
+ // Only preserve the key material: no algorithm and no comment
+ Files.write(sitePaths.peer_keys, parts[1].getBytes(ISO_8859_1));
+ assertContent(execGerritVersionCommand());
+
+ // Wipe out the content of the peer keys file
+ Files.delete(sitePaths.peer_keys);
+ assertThrows(IOException.class, () -> execGerritVersionCommand());
+ }
+ }
+
+ private String execGerritVersionCommand() throws Exception {
+ return execute(
+ ImmutableList.<String>builder()
+ .add(SSH_COMMAND)
+ .add(sitePaths.data_dir.resolve("id_rsa").toString())
+ .add("-p " + sshAddress.getPort())
+ .add(PeerDaemonUser.USER_NAME + "@" + sshAddress.getHostName())
+ .add("suexec")
+ .add("--as")
+ .add("admin")
+ .add("--")
+ .add("gerrit")
+ .add("version")
+ .build());
+ }
+
+ private String execute(ImmutableList<String> cmd) throws Exception {
+ return execute(cmd, sitePaths.data_dir.toFile(), ImmutableMap.of());
+ }
+
+ private static void assertContent(String result) {
+ assertThat(result).contains("gerrit version " + Version.getVersion());
+ }
+}
diff --git a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
index fb3a0db..ddcfe0c 100644
--- a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
+++ b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
@@ -151,7 +151,7 @@
// This is the loader that we configure for the cache when calling .loader(...)
@SuppressWarnings("unchecked")
- CacheLoader<String, String> baseLoader = (CacheLoader<String, String>) mock(CacheLoader.class);
+ CacheLoader<String, String> baseLoader = mock(CacheLoader.class);
resetLoaderAndAnswerLoadAndRefreshCalls(baseLoader);
// We wrap baseLoader just like H2CacheFactory is wrapping it. The wrapped version will call out
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index 8ffcc8b..5bfe97c 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -519,7 +519,7 @@
ChangeNoteUtil noteUtil = injector.getInstance(ChangeNoteUtil.class);
return writeCommit(
body,
- noteUtil.newIdent(changeOwner.getAccount().id(), TimeUtil.nowTs(), serverIdent),
+ noteUtil.newAccountIdIdent(changeOwner.getAccount().id(), TimeUtil.nowTs(), serverIdent),
false);
}
@@ -531,7 +531,7 @@
ChangeNoteUtil noteUtil = injector.getInstance(ChangeNoteUtil.class);
return writeCommit(
body,
- noteUtil.newIdent(changeOwner.getAccount().id(), TimeUtil.nowTs(), serverIdent),
+ noteUtil.newAccountIdIdent(changeOwner.getAccount().id(), TimeUtil.nowTs(), serverIdent),
initWorkInProgress);
}
diff --git a/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java b/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
index 5a28404..daefd7c 100644
--- a/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
+++ b/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
@@ -14,77 +14,126 @@
package com.google.gerrit.server.restapi.change;
+import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
-import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CommentsUtil;
import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class ListChangeCommentsTest {
-
@Test
- public void commentsLinkedToChangeMessages() {
- /**
- * Human comments should not be linked to auto-generated messages. A comment is linked to the
- * nearest next change message in timestamp
- */
- CommentInfo c1 = getNewCommentInfo("c1", Timestamp.valueOf("2018-01-01 09:01:00"));
- CommentInfo c2 = getNewCommentInfo("c2", Timestamp.valueOf("2018-01-01 09:01:15"));
- CommentInfo c3 = getNewCommentInfo("c3", Timestamp.valueOf("2018-01-01 09:01:25"));
+ public void commentsLinkedToChangeMessagesIgnoreGerritAutoGenTaggedMessages() {
+ /* Comments should not be linked to Gerrit's autogenerated messages */
+ List<CommentInfo> comments = createComments("c1", "00", "c2", "10", "c3", "25");
+ List<ChangeMessage> changeMessages =
+ createChangeMessages("cm1", "00", "cm2", "16", "cm3", "30");
- ChangeMessage cm1 =
- getNewChangeMessage("cm1key", "cm1", Timestamp.valueOf("2018-01-01 00:00:00"), null);
- ChangeMessage cmIgnore =
- getNewChangeMessage(
- "cm2key",
- "cm2",
- Timestamp.valueOf("2018-01-01 09:01:15"),
- ChangeMessagesUtil.AUTOGENERATED_TAG_PREFIX);
- ChangeMessage cm2 =
- getNewChangeMessage("cm2key", "cm2", Timestamp.valueOf("2018-01-01 09:01:16"), null);
- ChangeMessage cm3 =
- getNewChangeMessage("cm3key", "cm3", Timestamp.valueOf("2018-01-01 09:01:27"), null);
-
- assertThat(c1.changeMessageId).isNull();
- assertThat(c2.changeMessageId).isNull();
- assertThat(c3.changeMessageId).isNull();
-
- ImmutableList<CommentInfo> comments = ImmutableList.of(c1, c2, c3);
- ImmutableList<ChangeMessage> changeMessages = ImmutableList.of(cm1, cmIgnore, cm2, cm3);
+ changeMessages.add(
+ newChangeMessage("ignore", "cmAutoGenByGerrit", "15", ChangeMessagesUtil.TAG_MERGED));
CommentsUtil.linkCommentsToChangeMessages(comments, changeMessages, true);
- assertThat(c1.changeMessageId).isEqualTo(changeMessageKey(cm2));
- /** comment 2 ignored the auto-generated change message */
- assertThat(c2.changeMessageId).isEqualTo(changeMessageKey(cm2));
- assertThat(c3.changeMessageId).isEqualTo(changeMessageKey(cm3));
+ assertThat(getComment(comments, "c1").changeMessageId)
+ .isEqualTo(getChangeMessage(changeMessages, "cm1").getKey().uuid());
+ /* comment 2 ignored the auto-generated message because it has a Gerrit tag */
+ assertThat(getComment(comments, "c2").changeMessageId)
+ .isEqualTo(getChangeMessage(changeMessages, "cm2").getKey().uuid());
+ assertThat(getComment(comments, "c3").changeMessageId)
+ .isEqualTo(getChangeMessage(changeMessages, "cm3").getKey().uuid());
+
+ // Make sure no comment is linked to the auto-gen message
+ assertThat(comments.stream().map(c -> c.changeMessageId).collect(Collectors.toSet()))
+ .doesNotContain(getChangeMessage(changeMessages, "cmAutoGenByGerrit"));
}
- private static CommentInfo getNewCommentInfo(String message, Timestamp ts) {
+ @Test
+ public void commentsLinkedToChangeMessagesAllowLinkingToAutoGenTaggedMessages() {
+ /* Human comments are allowed to be linked to autogenerated messages */
+ List<CommentInfo> comments = createComments("c1", "00", "c2", "10", "c3", "25");
+ List<ChangeMessage> changeMessages =
+ createChangeMessages("cm1", "00", "cm2", "16", "cm3", "30");
+
+ changeMessages.add(
+ newChangeMessage(
+ "cmAutoGen", "cmAutoGen", "15", ChangeMessagesUtil.AUTOGENERATED_TAG_PREFIX));
+
+ CommentsUtil.linkCommentsToChangeMessages(comments, changeMessages, true);
+
+ assertThat(getComment(comments, "c1").changeMessageId)
+ .isEqualTo(getChangeMessage(changeMessages, "cm1").getKey().uuid());
+ /* comment 2 did not ignore the auto-generated change message */
+ assertThat(getComment(comments, "c2").changeMessageId)
+ .isEqualTo(getChangeMessage(changeMessages, "cmAutoGen").getKey().uuid());
+ assertThat(getComment(comments, "c3").changeMessageId)
+ .isEqualTo(getChangeMessage(changeMessages, "cm3").getKey().uuid());
+ }
+
+ /**
+ * Create a list of comments from the specified args args should be passed as consecutive pairs of
+ * messages and timestamps example: (m1, t1, m2, t2, ...)
+ */
+ private static List<CommentInfo> createComments(String... args) {
+ List<CommentInfo> comments = new ArrayList<>();
+ for (int i = 0; i < args.length; i += 2) {
+ String message = args[i];
+ String ts = args[i + 1];
+ comments.add(newCommentInfo(message, ts));
+ }
+ return comments;
+ }
+
+ /**
+ * Create a list of change messages from the specified args args should be passed as consecutive
+ * pairs of messages and timestamps example: (m1, t1, m2, t2, ...). the tag parameter for the
+ * created change messages will be null.
+ */
+ private static List<ChangeMessage> createChangeMessages(String... args) {
+ List<ChangeMessage> changeMessages = new ArrayList<>();
+ for (int i = 0; i < args.length; i += 2) {
+ String key = args[i] + "Key";
+ String message = args[i];
+ String ts = args[i + 1];
+ changeMessages.add(newChangeMessage(key, message, ts, null));
+ }
+ return changeMessages;
+ }
+
+ /** Create a new CommentInfo with a given message and timestamp */
+ private static CommentInfo newCommentInfo(String message, String ts) {
CommentInfo c = new CommentInfo();
c.message = message;
- c.updated = ts;
+ c.updated = Timestamp.valueOf("2000-01-01 00:00:" + ts);
return c;
}
- private static ChangeMessage getNewChangeMessage(
- String id, String message, Timestamp ts, String tag) {
+ /** Create a new change message with an id, message, timestamp and tag */
+ private static ChangeMessage newChangeMessage(String id, String message, String ts, String tag) {
ChangeMessage.Key key = ChangeMessage.key(Change.id(1), id);
- ChangeMessage cm = new ChangeMessage(key, null, ts, null);
+ ChangeMessage cm =
+ new ChangeMessage(key, null, Timestamp.valueOf("2000-01-01 00:00:" + ts), null);
cm.setMessage(message);
cm.setTag(tag);
return cm;
}
- private static String changeMessageKey(ChangeMessage changeMessage) {
- return changeMessage.getKey().uuid();
+ /** Return the change message from the list of messages that has specific message text */
+ private static ChangeMessage getChangeMessage(List<ChangeMessage> messages, String messageText) {
+ return messages.stream().filter(m -> m.getMessage().equals(messageText)).collect(onlyElement());
+ }
+
+ /** Return the comment from the list of comments that has specific message text */
+ private CommentInfo getComment(List<CommentInfo> comments, String messageText) {
+ return comments.stream().filter(c -> c.message.equals(messageText)).collect(onlyElement());
}
}
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionCheckTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionCheckTest.java
new file mode 100644
index 0000000..a5fd4a2
--- /dev/null
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionCheckTest.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.testing.InMemoryRepositoryManager;
+import com.google.inject.ProvisionException;
+import java.io.IOException;
+import java.nio.file.Paths;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+
+public class NoteDbSchemaVersionCheckTest {
+ private NoteDbSchemaVersionManager versionManager;
+ private SitePaths sitePaths;
+
+ @Before
+ public void setup() throws Exception {
+ AllProjectsName allProjectsName = new AllProjectsName("All-Projects");
+ GitRepositoryManager repoManager = new InMemoryRepositoryManager();
+ repoManager.createRepository(allProjectsName);
+ versionManager = new NoteDbSchemaVersionManager(allProjectsName, repoManager);
+ versionManager.init();
+
+ sitePaths = new SitePaths(Paths.get("/tmp/foo"));
+ }
+
+ @Test
+ public void shouldNotFailIfCurrentVersionIsExpected() {
+ new NoteDbSchemaVersionCheck(versionManager, sitePaths, new Config()).start();
+ // No exceptions should be thrown
+ }
+
+ @Test
+ public void shouldFailIfCurrentVersionIsOneMoreThanExpected() throws IOException {
+ versionManager.increment(NoteDbSchemaVersions.LATEST);
+
+ ProvisionException e =
+ assertThrows(
+ ProvisionException.class,
+ () -> new NoteDbSchemaVersionCheck(versionManager, sitePaths, new Config()).start());
+
+ assertThat(e)
+ .hasMessageThat()
+ .contains("Unsupported schema version " + (NoteDbSchemaVersions.LATEST + 1));
+ }
+
+ @Test
+ public void
+ shouldNotFailWithExperimentalRollingUpgradeEnabledAndCurrentVersionIsOneMoreThanExpected()
+ throws IOException {
+ Config gerritConfig = new Config();
+ gerritConfig.setBoolean("gerrit", null, "experimentalRollingUpgrade", true);
+ versionManager.increment(NoteDbSchemaVersions.LATEST);
+
+ NoteDbSchemaVersionCheck versionCheck =
+ new NoteDbSchemaVersionCheck(versionManager, sitePaths, gerritConfig);
+ versionCheck.start();
+ // No exceptions should be thrown
+ }
+}
diff --git a/lib/BUILD b/lib/BUILD
index f0c0aad..d3ef4b9 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -47,6 +47,13 @@
)
java_library(
+ name = "jgit-ssh-jsch",
+ data = ["//lib:LICENSE-jgit"],
+ visibility = ["//visibility:public"],
+ exports = ["@jgit//org.eclipse.jgit.ssh.jsch:ssh-jsch"],
+)
+
+java_library(
name = "jgit-archive",
data = ["//lib:LICENSE-jgit"],
visibility = ["//visibility:public"],
diff --git a/lib/nongoogle_test.sh b/lib/nongoogle_test.sh
index 95db5cc..0cdad1a 100755
--- a/lib/nongoogle_test.sh
+++ b/lib/nongoogle_test.sh
@@ -16,6 +16,9 @@
duct-tape
eddsa
elasticsearch-rest-client
+flogger
+flogger-log4j-backend
+flogger-system-backend
httpasyncclient
httpcore-nio
j2objc
diff --git a/modules/jgit b/modules/jgit
index 75fccca..8e79d5a 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit 75fcccaea39f7a2112886e04a94458d6b7b7c37f
+Subproject commit 8e79d5a290843b929f073a536a0d678fc74382ca
diff --git a/plugins/BUILD b/plugins/BUILD
index 5f9c142..a071bde 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -71,6 +71,7 @@
"//lib/jackson:jackson-core",
"//lib:jgit-servlet",
"//lib:jgit",
+ "//lib:jgit-ssh-jsch",
"//lib:jsr305",
"//lib/log:api",
"//lib/log:log4j",
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index e211fb1..7357ab4 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit e211fb1bd21043e2574c438a687c8f492d538c97
+Subproject commit 7357ab473599d16ae33cc982bbd65472f08c2dd6
diff --git a/plugins/delete-project b/plugins/delete-project
index 02cced3..7671def 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit 02cced37fd755a1123b1ec18af96503683d88f50
+Subproject commit 7671def07882aab89b19eb7496418588ea7375d9
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index 021ce08..7bca96d 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -1,6 +1,5 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary")
load("//tools/bzl:genrule2.bzl", "genrule2")
-load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
package(default_visibility = ["//visibility:public"])
diff --git a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.js b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.js
index b8d54a4..46dfaf2 100644
--- a/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.js
+++ b/polygerrit-ui/app/behaviors/dom-util-behavior/dom-util-behavior.js
@@ -15,6 +15,8 @@
* limitations under the License.
*/
+import {descendedFromClass} from '../../utils/dom-util.js';
+
export const DomUtilBehavior = {
/**
* Are any ancestors of the element (or the element itself) members of the
@@ -28,13 +30,9 @@
* @return {boolean}
*/
descendedFromClass(element, className, opt_stopElement) {
- let isDescendant = element.classList.contains(className);
- while (!isDescendant && element.parentElement &&
- (!opt_stopElement || element.parentElement !== opt_stopElement)) {
- isDescendant = element.classList.contains(className);
- element = element.parentElement;
- }
- return isDescendant;
+ console.warn('DomUtilBehavior is deprecated.' +
+ 'Use descendedFromClass from utils directly.');
+ return descendedFromClass(element, className, opt_stopElement);
},
};
diff --git a/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.js b/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.js
deleted file mode 100644
index 88c8835..0000000
--- a/polygerrit-ui/app/behaviors/fire-behavior/fire-behavior.js
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * @license
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/** @polymerBehavior Gerrit.FireBehavior */
-export const FireBehavior = {
- /**
- * Dispatches a custom event with an optional detail value.
- *
- * @param {string} type Name of event type.
- * @param {*=} detail Detail value containing event-specific
- * payload.
- * @param {{ bubbles: (boolean|undefined), cancelable: (boolean|undefined),
- * composed: (boolean|undefined) }=}
- * options Object specifying options. These may include:
- * `bubbles` (boolean, defaults to `true`),
- * `cancelable` (boolean, defaults to false), and
- * `composed` (boolean, defaults to true).
- * @return {!Event} The new event that was fired.
- * @override
- */
- fire(type, detail, options) {
- console.warn('\'fire\' is deprecated, please use dispatchEvent instead!');
- options = options || {};
- detail = (detail === null || detail === undefined) ? {} : detail;
- const event = new Event(type, {
- bubbles: options.bubbles === undefined ? true : options.bubbles,
- cancelable: Boolean(options.cancelable),
- composed: options.composed === undefined ? true: options.composed,
- });
- event.detail = detail;
- this.dispatchEvent(event);
- return event;
- },
-};
-
-// TODO(dmfilippov) Remove the following lines with assignments
-// Plugins can use the behavior because it was accessible with
-// the global Gerrit... variable. To avoid breaking changes in plugins
-// temporary assign global variables.
-window.Gerrit = window.Gerrit || {};
-window.Gerrit.FireBehavior = FireBehavior;
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
index 09e3b72..ca31ee8 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
@@ -171,6 +171,7 @@
TOGGLE_FILE_REVIEWED: 'TOGGLE_FILE_REVIEWED',
TOGGLE_ALL_INLINE_DIFFS: 'TOGGLE_ALL_INLINE_DIFFS',
TOGGLE_INLINE_DIFF: 'TOGGLE_INLINE_DIFF',
+ TOGGLE_HIDE_ALL_COMMENT_THREADS: 'TOGGLE_HIDE_ALL_COMMENT_THREADS',
OPEN_FIRST_FILE: 'OPEN_FIRST_FILE',
OPEN_LAST_FILE: 'OPEN_LAST_FILE',
@@ -251,6 +252,8 @@
'Expand all comment threads');
_describe(Shortcut.COLLAPSE_ALL_COMMENT_THREADS, ShortcutSection.DIFFS,
'Collapse all comment threads');
+_describe(Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, ShortcutSection.DIFFS,
+ 'Hide/Display all comment threads');
_describe(Shortcut.LEFT_PANE, ShortcutSection.DIFFS, 'Select left pane');
_describe(Shortcut.RIGHT_PANE, ShortcutSection.DIFFS, 'Select right pane');
_describe(Shortcut.TOGGLE_LEFT_PANE, ShortcutSection.DIFFS,
@@ -290,6 +293,8 @@
'Go to selected file');
_describe(Shortcut.TOGGLE_ALL_INLINE_DIFFS, ShortcutSection.FILE_LIST,
'Show/hide all inline diffs');
+_describe(Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, ShortcutSection.FILE_LIST,
+ 'Hide/Display all comment threads');
_describe(Shortcut.TOGGLE_INLINE_DIFF, ShortcutSection.FILE_LIST,
'Show/hide selected inline diff');
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js
index c46cf30..e9c16241 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js
@@ -82,7 +82,7 @@
<div id="mainContainer">
<div class="header">
<div class="name">
- <h3>[[_computeSectionName(section.id)]]</h3>
+ <h3 class="heading-3">[[_computeSectionName(section.id)]]</h3>
<gr-button
id="editBtn"
link=""
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js
index c5577c2..47657ac 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js
@@ -63,9 +63,9 @@
Loading...
</div>
<div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
- <h1 id="Title">[[_groupName]]</h1>
+ <h1 id="Title" class="heading-1">[[_groupName]]</h1>
<div id="form">
- <h3 id="members">Members</h3>
+ <h3 id="members" class="heading-3">Members</h3>
<fieldset>
<span class="value">
<gr-autocomplete
@@ -112,7 +112,7 @@
</tbody>
</table>
</fieldset>
- <h3 id="includedGroups">Included Groups</h3>
+ <h3 id="includedGroups" class="heading-3">Included Groups</h3>
<fieldset>
<span class="value">
<gr-autocomplete
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js
index 0af87ad..7a843dc 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js
@@ -37,18 +37,21 @@
Loading...
</div>
<div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
- <h1 id="Title">[[_groupName]]</h1>
- <h2 id="configurations">General</h2>
+ <h1 id="Title" class="heading-1">[[_groupName]]</h1>
+ <h2 id="configurations" class="heading-2">General</h2>
<div id="form">
<fieldset>
- <h3 id="groupUUID">Group UUID</h3>
+ <h3 id="groupUUID" class="heading-3">Group UUID</h3>
<fieldset>
<gr-copy-clipboard
id="uuid"
text="[[_getGroupUUID(_groupConfig.id)]]"
></gr-copy-clipboard>
</fieldset>
- <h3 id="groupName" class$="[[_computeHeaderClass(_rename)]]">
+ <h3
+ id="groupName"
+ class$="heading-3 [[_computeHeaderClass(_rename)]]"
+ >
Group Name
</h3>
<fieldset>
@@ -72,7 +75,10 @@
>
</span>
</fieldset>
- <h3 id="groupOwner" class$="[[_computeHeaderClass(_owner)]]">
+ <h3
+ id="groupOwner"
+ class$="heading-3 [[_computeHeaderClass(_owner)]]"
+ >
Owners
</h3>
<fieldset>
@@ -99,7 +105,7 @@
>
</span>
</fieldset>
- <h3 class$="[[_computeHeaderClass(_description)]]">
+ <h3 class$="heading-3 [[_computeHeaderClass(_description)]]">
Description
</h3>
<fieldset>
@@ -123,7 +129,7 @@
</gr-button>
</span>
</fieldset>
- <h3 id="options" class$="[[_computeHeaderClass(_options)]]">
+ <h3 id="options" class$="heading-3 [[_computeHeaderClass(_options)]]">
Group Options
</h3>
<fieldset>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js
index 5f0739a..b46c7c0 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js
@@ -62,7 +62,10 @@
Loading...
</div>
<div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
- <h3 id="inheritsFrom" class$="[[_computeShowInherit(_inheritsFrom)]]">
+ <h3
+ id="inheritsFrom"
+ class$="heading-3 [[_computeShowInherit(_inheritsFrom)]]"
+ >
<span class="rightsText">Rights Inherit From</span>
<a
href$="[[_computeParentHref(_inheritsFrom.name)]]"
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
deleted file mode 100644
index d161423..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
+++ /dev/null
@@ -1,52 +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 '../../../styles/shared-styles.js';
-import '../../shared/gr-button/gr-button.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-repo-command_html.js';
-
-/** @extends PolymerElement */
-class GrRepoCommand extends GestureEventListeners(
- LegacyElementMixin(
- PolymerElement)) {
- static get template() { return htmlTemplate; }
-
- static get is() { return 'gr-repo-command'; }
-
- static get properties() {
- return {
- title: String,
- disabled: Boolean,
- tooltip: String,
- };
- }
-
- /**
- * Fired when command button is tapped.
- *
- * @event command-tap
- */
-
- _onCommandTap() {
- this.dispatchEvent(
- new CustomEvent('command-tap', {bubbles: true, composed: true}));
- }
-}
-
-customElements.define(GrRepoCommand.is, GrRepoCommand);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html
deleted file mode 100644
index a73f071..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html
+++ /dev/null
@@ -1,53 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-repo-command</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
- <template>
- <gr-repo-command></gr-repo-command>
- </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-repo-command.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-suite('gr-repo-command tests', () => {
- let element;
-
- setup(() => {
- element = fixture('basic');
- });
-
- test('dispatched command-tap on button tap', done => {
- element.addEventListener('command-tap', () => {
- done();
- });
- MockInteractions.tap(
- dom(element.root).querySelector('gr-button'));
- });
-});
-</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
index 03a2bd3..4ab1b98 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
@@ -24,7 +24,6 @@
import '../../shared/gr-overlay/gr-overlay.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../gr-create-change-dialog/gr-create-change-dialog.js';
-import '../gr-repo-command/gr-repo-command.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
@@ -60,6 +59,10 @@
/** @type {?} */
_repoConfig: Object,
_canCreate: Boolean,
+ // states
+ _creatingChange: Boolean,
+ _editingConfig: Boolean,
+ _runningGC: Boolean,
};
}
@@ -102,13 +105,17 @@
}
_handleRunningGC() {
+ this._runningGC = true;
return this.$.restAPI.runRepoGC(this.repo).then(response => {
if (response.status === 200) {
this.dispatchEvent(new CustomEvent(
'show-alert',
{detail: {message: GC_MESSAGE}, bubbles: true, composed: true}));
}
- });
+ })
+ .finally(() => {
+ this._runningGC = false;
+ });
}
_createNewChange() {
@@ -116,7 +123,11 @@
}
_handleCreateChange() {
- this.$.createNewChangeModal.handleCreateChange();
+ this._creatingChange = true;
+ this.$.createNewChangeModal.handleCreateChange()
+ .finally(() => {
+ this._creatingChange = false;
+ });
this._handleCloseCreateChange();
}
@@ -125,6 +136,7 @@
}
_handleEditRepoConfig() {
+ this._editingConfig = true;
return this.$.restAPI.createChange(this.repo, CONFIG_BRANCH,
EDIT_CONFIG_SUBJECT, undefined, false, true).then(change => {
const message = change ?
@@ -136,7 +148,10 @@
GerritNav.navigateToRelativeUrl(GerritNav.getEditUrlForDiff(
change, CONFIG_PATH, INITIAL_PATCHSET));
- });
+ })
+ .finally(() => {
+ this._editingConfig = false;
+ });
}
}
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js
index b27c36b..3ae5b29 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js
@@ -24,34 +24,41 @@
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<style include="gr-form-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
+ #form gr-button {
+ margin-bottom: var(--spacing-xxl);
+ }
</style>
<main class="gr-form-styles read-only">
- <h1 id="Title">Repository Commands</h1>
+ <h1 id="Title" class="heading-1">Repository Commands</h1>
<div id="loading" class$="[[_computeLoadingClass(_loading)]]">
Loading...
</div>
<div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
- <h2 id="options">Command</h2>
+ <h2 id="options" class="heading-2">Command</h2>
<div id="form">
- <gr-repo-command
- title="Create change"
- on-command-tap="_createNewChange"
- >
- </gr-repo-command>
- <gr-repo-command
+ <h3>Create change</h3>
+ <gr-button loading="[[_creatingChange]]" on-click="_createNewChange">
+ Create change
+ </gr-button>
+ <h3>Edit repo config</h3>
+ <gr-button
id="editRepoConfig"
- title="Edit repo config"
- on-command-tap="_handleEditRepoConfig"
+ loading="[[_editingConfig]]"
+ on-click="_handleEditRepoConfig"
>
- </gr-repo-command>
- <gr-repo-command
- title="[[_repoConfig.actions.gc.label]]"
- tooltip="[[_repoConfig.actions.gc.title]]"
- hidden$="[[!_repoConfig.actions.gc.enabled]]"
- on-command-tap="_handleRunningGC"
+ Edit repo config
+ </gr-button>
+ <h3 hidden="[[!_repoConfig.actions.gc.enabled]]">
+ [[_repoConfig.actions.gc.label]]
+ </h3>
+ <gr-button
+ hidden="[[!_repoConfig.actions.gc.enabled]]"
+ title="[[_repoConfig.actions.gc.title]]"
+ loading="[[_runningGC]]"
+ on-click="_handleRunningGC"
>
- </gr-repo-command>
+ [[_repoConfig.actions.gc.label]]
+ </gr-button>
<gr-endpoint-decorator name="repo-command">
<gr-endpoint-param name="config" value="[[_repoConfig]]">
</gr-endpoint-param>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
index db2bfcf..a52ab92 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
@@ -99,8 +99,8 @@
test('successful creation of change', () => {
const change = {_number: '1'};
createChangeStub.returns(Promise.resolve(change));
- MockInteractions.tap(element.$.editRepoConfig.shadowRoot
- .querySelector('gr-button'));
+ MockInteractions.tap(element.$.editRepoConfig);
+ assert.isTrue(element.$.editRepoConfig.loading);
return handleSpy.lastCall.returnValue.then(() => {
flushAsynchronousOperations();
@@ -110,13 +110,14 @@
assert.isTrue(urlStub.called);
assert.deepEqual(urlStub.lastCall.args,
[change, 'project.config', 1]);
+ assert.isFalse(element.$.editRepoConfig.loading);
});
});
test('unsuccessful creation of change', () => {
createChangeStub.returns(Promise.resolve(null));
- MockInteractions.tap(element.$.editRepoConfig.shadowRoot
- .querySelector('gr-button'));
+ MockInteractions.tap(element.$.editRepoConfig);
+ assert.isTrue(element.$.editRepoConfig.loading);
return handleSpy.lastCall.returnValue.then(() => {
flushAsynchronousOperations();
@@ -124,6 +125,7 @@
assert.equal(alertStub.lastCall.args[0].detail.message,
'Failed to create change.');
assert.isFalse(urlStub.called);
+ assert.isFalse(element.$.editRepoConfig.loading);
});
});
});
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js
index de36e73..ea8a8b9 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js
@@ -50,7 +50,7 @@
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<div class="info">
- <h1 id="Title" class$="name">
+ <h1 id="Title" class="heading-1">
[[repo]]
<hr />
</h1>
@@ -63,7 +63,7 @@
</div>
<div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
<div id="downloadContent" class$="[[_computeHideClass(_schemes)]]">
- <h2 id="download">Download</h2>
+ <h2 id="download" class="heading-2">Download</h2>
<fieldset>
<gr-download-commands
id="downloadCommands"
@@ -73,12 +73,15 @@
></gr-download-commands>
</fieldset>
</div>
- <h2 id="configurations" class$="[[_computeHeaderClass(_configChanged)]]">
+ <h2
+ id="configurations"
+ class$="heading-2 [[_computeHeaderClass(_configChanged)]]"
+ >
Configurations
</h2>
<div id="form">
<fieldset>
- <h3 id="Description">Description</h3>
+ <h3 id="Description" class="heading-3">Description</h3>
<fieldset>
<iron-autogrow-textarea
id="descriptionInput"
@@ -89,7 +92,7 @@
disabled$="[[_readOnly]]"
></iron-autogrow-textarea>
</fieldset>
- <h3 id="Options">Repository Options</h3>
+ <h3 id="Options" class="heading-3">Repository Options</h3>
<fieldset id="options">
<section>
<span class="title">State</span>
@@ -362,7 +365,7 @@
</span>
</section>
</fieldset>
- <h3 id="Options">Contributor Agreements</h3>
+ <h3 id="Options" class="heading-3">Contributor Agreements</h3>
<fieldset id="agreements">
<section>
<span class="title">
@@ -407,7 +410,7 @@
class$="pluginConfig [[_computeHideClass(_pluginData)]]"
on-plugin-config-changed="_handlePluginConfigChanged"
>
- <h3>Plugins</h3>
+ <h3 class="heading-3">Plugins</h3>
<template is="dom-repeat" items="[[_pluginData]]" as="data">
<gr-repo-plugin-config
plugin-data="[[data]]"
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js
index 4a357af..cc6223d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js
@@ -48,12 +48,6 @@
padding-top: var(--spacing-xl);
vertical-align: top;
}
- #help h1 {
- font-family: var(--header-font-family);
- font-size: var(--font-size-h3);
- font-weight: var(--font-weight-h3);
- line-height: var(--line-height-h3);
- }
#help p {
margin-bottom: var(--spacing-m);
max-width: 35em;
@@ -73,7 +67,7 @@
</p>
</div>
<div id="help">
- <h1>Push your first change for code review</h1>
+ <h2 class="heading-3">Push your first change for code review</h2>
<p>
Pushing a change for review is easy, but a little different from other git
code review tools. Click on the \`Create Change' button and follow the
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js
index f6fb1d0..75af51e 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js
@@ -24,7 +24,7 @@
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<div class="info">
- <h1 class$="name">
+ <h1 class="heading-1">
[[repo]]
<hr />
</h1>
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js
index 5a5d590..8207284 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js
@@ -21,12 +21,6 @@
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<style include="dashboard-header-styles">
- .name {
- display: inline-block;
- }
- .name hr {
- width: 100%;
- }
.status.hide,
.name.hide,
.dashboardLink.hide {
@@ -39,7 +33,7 @@
aria-label="Account avatar"
></gr-avatar>
<div class="info">
- <h1 class="name">
+ <h1 class="heading-1">
[[_computeDetail(_accountDetails, 'name')]]
</h1>
<hr />
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
index 78627a7..d2d6037 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
@@ -104,7 +104,7 @@
}
_computeRequirementIcon(requirementStatus) {
- return requirementStatus ? 'gr-icons:check' : 'gr-icons:hourglass';
+ return requirementStatus ? 'gr-icons:check' : 'gr-icons:schedule';
}
_computeLabels(labelsRecord) {
@@ -132,7 +132,7 @@
_computeLabelIcon(labelInfo) {
if (labelInfo.approved) { return 'gr-icons:check'; }
if (labelInfo.rejected) { return 'gr-icons:close'; }
- return 'gr-icons:hourglass';
+ return 'gr-icons:schedule';
}
/**
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
index e100f91..276e821 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
@@ -51,13 +51,13 @@
assert.equal(element._computeRequirementIcon(true), 'gr-icons:check');
assert.equal(element._computeRequirementIcon(false),
- 'gr-icons:hourglass');
+ '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:hourglass');
+ assert.equal(element._computeLabelIcon({}), 'gr-icons:schedule');
assert.equal(element._computeLabelClass({approved: []}), 'approved');
assert.equal(element._computeLabelClass({rejected: []}), 'rejected');
@@ -90,7 +90,7 @@
assert.equal(element._requiredLabels.length, 1);
assert.equal(element._optionalLabels[0].label, 'opt_test');
- assert.equal(element._optionalLabels[0].icon, 'gr-icons:hourglass');
+ assert.equal(element._optionalLabels[0].icon, 'gr-icons:schedule');
assert.equal(element._optionalLabels[0].style, '');
assert.ok(element._optionalLabels[0].labelInfo);
});
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index f8580e0..799adde 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -57,7 +57,7 @@
import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
import {GrEditConstants} from '../../edit/gr-edit-constants.js';
import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
-import {util} from '../../../scripts/util.js';
+import {getComputedStyleValue} from '../../../utils/dom-util.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
import {pluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
@@ -1019,10 +1019,7 @@
this.$.fileList.collapseAllDiffs();
}
- _paramsChanged(value, oldValue) {
- const paramsChanged = JSON.stringify(oldValue) !== JSON.stringify(value);
- if (!paramsChanged) return;
-
+ _paramsChanged(value) {
if (value.view !== GerritNav.View.CHANGE) {
this._initialLoadComplete = false;
return;
@@ -1138,6 +1135,14 @@
this.viewState.numFilesShown = numFilesShown;
}
+ _handleMessageAnchorTap(e) {
+ const hash = MSG_PREFIX + e.detail.id;
+ const url = GerritNav.getUrlForChange(this._change,
+ this._patchRange.patchNum, this._patchRange.basePatchNum,
+ this._editMode, hash);
+ history.replaceState(null, '', url);
+ }
+
_maybeScrollToMessage(hash) {
if (hash.startsWith(MSG_PREFIX)) {
this.messagesList.scrollToMessage(hash.substr(MSG_PREFIX.length));
@@ -1998,7 +2003,7 @@
_computeShowRelatedToggle() {
// Make sure the max height has been applied, since there is now content
// to populate.
- if (!util.getComputedStyleValue('--relation-chain-max-height', this)) {
+ if (!getComputedStyleValue('--relation-chain-max-height', this)) {
this._updateRelatedChangeMaxHeight();
}
// Prevents showMore from showing when click on related change, since the
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
index 50b6d2d..f5a0463 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
@@ -711,6 +711,7 @@
change-comments="[[_changeComments]]"
project-name="[[_change.project]]"
show-reply-buttons="[[_loggedIn]]"
+ on-message-anchor-tap="_handleMessageAnchorTap"
on-reply="_handleMessageReply"
></gr-messages-list>
</template>
@@ -725,6 +726,7 @@
change-comments="[[_changeComments]]"
project-name="[[_change.project]]"
show-reply-buttons="[[_loggedIn]]"
+ on-message-anchor-tap="_handleMessageAnchorTap"
on-reply="_handleMessageReply"
></gr-messages-list-experimental>
</template>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
index 192ba8f..faa81b6 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
@@ -24,7 +24,7 @@
import {KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
import {GrEditConstants} from '../../edit/gr-edit-constants.js';
import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
-import {util} from '../../../scripts/util.js';
+import {getComputedStyleValue} from '../../../utils/dom-util.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
@@ -320,7 +320,21 @@
});
const getCustomCssValue =
- cssParam => util.getComputedStyleValue(cssParam, element);
+ cssParam => getComputedStyleValue(cssParam, element);
+
+ test('_handleMessageAnchorTap', () => {
+ element._changeNum = '1';
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 1,
+ };
+ const getUrlStub = sandbox.stub(GerritNav, 'getUrlForChange');
+ const replaceStateStub = sandbox.stub(history, 'replaceState');
+ element._handleMessageAnchorTap({detail: {id: 'a12345'}});
+
+ assert.equal(getUrlStub.lastCall.args[4], '#message-a12345');
+ assert.isTrue(replaceStateStub.called);
+ });
suite('plugins adding to file tab', () => {
setup(done => {
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js
index 9569c03..0446e4e 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js
@@ -56,16 +56,12 @@
.archives a:last-of-type {
margin-right: 0;
}
- .title {
- flex: 1;
- font-weight: var(--font-weight-bold);
- }
.hidden {
display: none;
}
</style>
<section>
- <h3 class="title">
+ <h3 class="heading-3">
Patch set [[patchNum]] of [[_computePatchSetQuantity(change.revisions)]]
</h3>
</section>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index adcf2e2..fbae39f 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -70,6 +70,8 @@
U: 'Unchanged',
};
+const FILE_ROW_CLASS = 'file-row';
+
/**
* Type for FileInfo
*
@@ -273,6 +275,8 @@
[this.Shortcut.RIGHT_PANE]: '_handleRightPane',
[this.Shortcut.TOGGLE_INLINE_DIFF]: '_handleToggleInlineDiff',
[this.Shortcut.TOGGLE_ALL_INLINE_DIFFS]: '_handleToggleAllInlineDiffs',
+ [this.Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS]:
+ '_handleToggleHideAllCommentThreads',
[this.Shortcut.CURSOR_NEXT_FILE]: '_handleCursorNext',
[this.Shortcut.CURSOR_PREV_FILE]: '_handleCursorPrev',
[this.Shortcut.NEXT_LINE]: '_handleCursorNext',
@@ -676,29 +680,55 @@
}
/**
+ * Returns true if the event e is a click on an element.
+ *
+ * The click is: mouse click or pressing Enter or Space key
+ * P.S> Screen readers sends click event as well
+ */
+ _isClickEvent(e) {
+ if (e.type === 'click') {
+ return true;
+ }
+ const isSpaceOrEnter = (e.key === 'Enter' || e.key === ' ');
+ return e.type === 'keydown' && isSpaceOrEnter;
+ }
+
+ _fileActionClick(e, fileAction) {
+ 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
+ e.stopPropagation();
+ this.$.fileCursor.setCursor(fileRow.element);
+ fileAction(fileRow.file);
+ }
+ }
+
+ _reviewedClick(e) {
+ this._fileActionClick(e,
+ file => this._reviewFile(file.path));
+ }
+
+ _expandedClick(e) {
+ this._fileActionClick(e,
+ file => this._toggleFileExpanded(file));
+ }
+
+ /**
* Handle all events from the file list dom-repeat so event handleers don't
* have to get registered for potentially very long lists.
*/
_handleFileListClick(e) {
- // Traverse upwards to find the row element if the target is not the row.
- let row = e.target;
- while (!row.classList.contains('row') && row.parentElement) {
- row = row.parentElement;
- }
-
- // No action needed for item without a valid file
- if (!row.dataset.file) {
+ const fileRow = this._getFileRowFromEvent(e);
+ if (!fileRow) {
return;
}
-
- const file = JSON.parse(row.dataset.file);
+ const file = fileRow.file;
const path = file.path;
- // Handle checkbox mark as reviewed.
- if (e.target.classList.contains('markReviewed')) {
- e.preventDefault();
- return this._reviewFile(path);
- }
-
// If a path cannot be interpreted from the click target (meaning it's not
// somewhere in the row, e.g. diff content) or if the user clicked the
// link, defer to the native behavior.
@@ -708,9 +738,28 @@
if (this.descendedFromClass(e.target, 'editFileControls')) { return; }
e.preventDefault();
+ this.$.fileCursor.setCursor(fileRow.element);
this._toggleFileExpanded(file);
}
+ _getFileRowFromEvent(e) {
+ // Traverse upwards to find the row element if the target is not the row.
+ let row = e.target;
+ while (!row.classList.contains(FILE_ROW_CLASS) && row.parentElement) {
+ row = row.parentElement;
+ }
+
+ // No action needed for item without a valid file
+ if (!row.dataset.file) {
+ return null;
+ }
+
+ return {
+ file: JSON.parse(row.dataset.file),
+ element: row,
+ };
+ }
+
/**
* Generates file range from file info object.
*
@@ -761,6 +810,15 @@
this._toggleInlineDiffs();
}
+ _handleToggleHideAllCommentThreads(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
+
+ e.preventDefault();
+ this.toggleClass('hideComments');
+ }
+
_handleCursorNext(e) {
if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
return;
@@ -1066,7 +1124,7 @@
if (this._files && this._files.length > 0) {
flush();
this.$.fileCursor.stops = Array.from(
- dom(this.root).querySelectorAll('.file-row'));
+ dom(this.root).querySelectorAll(`.${FILE_ROW_CLASS}`));
this.$.fileCursor.setCursorAtIndex(this.selectedIndex, true);
}
}
@@ -1129,10 +1187,29 @@
FileStatus[statusCode] : 'Status Unknown';
}
+ /**
+ * Converts any boolean-like variable to the string 'true' or 'false'
+ *
+ * This method is useful when you bind aria-checked attribute to a boolean
+ * value. The aria-checked attribute is string attribute. Binding directly
+ * to boolean variable causes problem on gerrit-CI.
+ *
+ * @param {object} val
+ * @return {string} 'true' if val is true-like, otherwise false
+ */
+ _booleanToString(val) {
+ return val ? 'true' : 'false';
+ }
+
_isFileExpanded(path, expandedFilesRecord) {
return expandedFilesRecord.base.some(f => f.path === path);
}
+ _isFileExpandedStr(path, expandedFilesRecord) {
+ return this._booleanToString(
+ this._isFileExpanded(path, expandedFilesRecord));
+ }
+
_computeExpandedFiles(expandedCount, totalCount) {
if (expandedCount === 0) {
return GrFileListConstants.FilesExpandedState.NONE;
@@ -1178,7 +1255,7 @@
}
this._updateDiffCursor();
- this.$.diffCursor.handleDiffUpdate();
+ this.$.diffCursor.reInitAndUpdateStops();
}
_clearCollapsedDiffs(collapsedDiffs) {
@@ -1202,6 +1279,14 @@
_renderInOrder(files, diffElements, initialCount) {
let iter = 0;
+ for (const file of files) {
+ const path = file.path;
+ const diffElem = this._findDiffByPath(path, diffElements);
+ if (diffElem) {
+ diffElem.prefetchDiff();
+ }
+ }
+
return (new Promise(resolve => {
this.dispatchEvent(new CustomEvent('reload-drafts', {
detail: {resolve},
@@ -1239,6 +1324,10 @@
* This also however results in the fact that the cursor does not auto
* focus on the first diff chunk on a small screen. This is however, a use
* case we are willing to not support for now.
+
+ * Using handleDiffUpdate resulted in diffCursor.row being set which
+ * prevented the issue of scrolling to top when we expand the second
+ * file individually.
*/
this.$.diffCursor.reInitAndUpdateStops();
}));
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
index d1238f3..a2714d9 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
@@ -28,6 +28,20 @@
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;
+ }
+ div[role='gridcell']
+ > div.comments
+ > span:empty
+ + span:empty
+ + span.noCommentsScreenReaderText {
+ display: inline;
+ }
:host(.loading) .row {
opacity: 0.5;
}
@@ -164,6 +178,9 @@
color: #c62828;
font-weight: var(--font-weight-bold);
}
+ .show-hide-icon:focus {
+ outline: none;
+ }
.show-hide {
margin-left: var(--spacing-s);
width: 1.9em;
@@ -200,27 +217,24 @@
margin-left: var(--spacing-xxl);
width: 15em;
}
- .reviewed label {
+ .reviewedSwitch {
color: var(--link-color);
opacity: 0;
justify-content: flex-end;
width: 100%;
}
- .reviewed label:hover {
+ .reviewedSwitch:hover {
cursor: pointer;
opacity: 100;
}
.row:focus {
outline: none;
}
- .row:hover .reviewed label,
- .row:focus .reviewed label,
- .row.expanded .reviewed label {
+ .row:hover .reviewedSwitch,
+ .row:focus-within .reviewedSwitch,
+ .row.expanded .reviewedSwitch {
opacity: 100;
}
- .reviewed input {
- display: none;
- }
.reviewedLabel {
color: var(--deemphasized-text-color);
margin-right: var(--spacing-l);
@@ -233,6 +247,9 @@
.editFileControls {
width: 7em;
}
+ .markReviewed:focus {
+ outline: none;
+ }
.markReviewed,
.pathLink {
display: inline-block;
@@ -255,10 +272,8 @@
padding: 0px;
}
}
- .pathLink:hover gr-copy-clipboard,
- .pathLink:focus gr-copy-clipboard,
- .oldPath:focus gr-copy-clipboard,
- .oldPath:hover gr-copy-clipboard {
+ .row:focus-within gr-copy-clipboard,
+ .row:hover gr-copy-clipboard {
visibility: visible;
}
@@ -295,9 +310,17 @@
display: none;
}
}
+ :host(.hideComments) {
+ --gr-comment-thread-display: none;
+ }
</style>
- <div id="container" on-click="_handleFileListClick">
- <div class="header-row row">
+ <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
@@ -305,14 +328,20 @@
items="[[_dynamicPrependedHeaderEndpoints]]"
as="headerEndpoint"
>
- <gr-endpoint-decorator name$="[[headerEndpoint]]">
+ <gr-endpoint-decorator 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-decorator>
</template>
</template>
- <div class="path">File</div>
- <div class="comments">Comments</div>
- <div class="sizeBars">Size</div>
- <div class="header-stats">Delta</div>
+ <div class="path" role="columnheader">File</div>
+ <div class="comments" role="columnheader">Comments</div>
+ <div class="sizeBars" 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
@@ -320,14 +349,18 @@
items="[[_dynamicHeaderEndpoints]]"
as="headerEndpoint"
>
- <gr-endpoint-decorator name$="[[headerEndpoint]]">
+ <gr-endpoint-decorator 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]]"></div>
- <div class="editFileControls showOnEdit"></div>
- <div class="show-hide"></div>
+ <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
@@ -344,6 +377,7 @@
class$="file-row row [[_computePathClass(file.__path, _expandedFiles.*)]]"
data-file$="[[_computeFileRange(file)]]"
tabindex="-1"
+ role="row"
>
<!-- endpoint: change-view-file-list-content-prepend -->
<template is="dom-if" if="[[_showPrependedDynamicColumns]]">
@@ -352,7 +386,9 @@
items="[[_dynamicPrependedContentEndpoints]]"
as="contentEndpoint"
>
- <gr-endpoint-decorator name="[[contentEndpoint]]">
+ <gr-endpoint-decorator 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]]">
@@ -366,6 +402,7 @@
<span
data-url="[[_computeDiffURL(change, patchRange, file.__path, editMode)]]"
class="path"
+ role="gridcell"
>
<a
class="pathLink"
@@ -406,66 +443,114 @@
</div>
</template>
</span>
- <div class="comments desktop">
- <span class="drafts">
- [[_computeDraftsString(changeComments, patchRange, file.__path)]]
- </span>
- [[_computeCommentsString(changeComments, patchRange, file.__path)]]
+ <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.__path)]]<!-- This comments ensure that span is empty when
+ the function returns empty string.
+ --></span
+ >
+ <span
+ ><!--
+ -->[[_computeCommentsString(changeComments, patchRange,
+ file.__path)]]<!--
+ --></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.__path)]]<!-- This comments ensure that span is empty when
+ the function returns empty string.
+ --></span
+ >
+ <span
+ ><!--
+ -->[[_computeCommentsStringMobile(changeComments, patchRange,
+ file.__path)]]<!--
+ --></span
+ >
+ <span class="noCommentsScreenReaderText">
+ <!-- The same as for desktop comments -->
+ No comments
+ </span>
+ </div>
</div>
- <div class="comments mobile">
- <span class="drafts">
- [[_computeDraftsStringMobile(changeComments, patchRange,
- file.__path)]]
- </span>
- [[_computeCommentsStringMobile(changeComments, patchRange,
- file.__path)]]
+ <div 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$="[[_computeSizeBarsClass(_showSizeBars, file.__path)]]"
- aria-label="A bar that represents the addition and deletion ratio for the current file"
- tabindex="0"
- >
- <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 class$="[[_computeClass('stats', 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 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]]">
@@ -474,8 +559,10 @@
items="[[_dynamicContentEndpoints]]"
as="contentEndpoint"
>
- <div class$="[[_computeClass('', file.__path)]]">
+ <div class$="[[_computeClass('', file.__path)]]" role="gridcell">
<gr-endpoint-decorator 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]]">
@@ -486,25 +573,44 @@
</div>
</template>
</template>
- <div class="reviewed hideOnEdit" hidden$="[[!_loggedIn]]" hidden="">
+ <div
+ class="reviewed hideOnEdit"
+ role="gridcell"
+ hidden$="[[!_loggedIn]]"
+ >
<span
class$="reviewedLabel [[_computeReviewedClass(file.isReviewed)]]"
+ aria-hidden$="[[!file.isReviewed]]"
>Reviewed</span
>
- <label>
- <input
- class="reviewed"
- type="checkbox"
- checked="[[file.isReviewed]]"
- />
+ <!-- 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
>
- </label>
+ </span>
</div>
- <div class="editFileControls showOnEdit">
+ <div
+ class="editFileControls showOnEdit"
+ role="gridcell"
+ aria-hidden$="[[!editMode]]"
+ >
<template is="dom-if" if="[[editMode]]">
<gr-edit-file-controls
class$="[[_computeClass('', file.__path)]]"
@@ -512,25 +618,32 @@
></gr-edit-file-controls>
</template>
</div>
- <div class="show-hide">
- <label
+ <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"
>
- <input
- type="checkbox"
- class="show-hide"
- checked$="[[_isFileExpanded(file.__path, _expandedFiles.*)]]"
- data-path$="[[file.__path]]"
- data-expand="true"
- />
+ <!-- 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>
- </label>
+ </span>
</div>
</div>
<template
@@ -560,14 +673,14 @@
<span
class="added"
tabindex="0"
- aria-label$="[[_patchChange.inserted]] lines added"
+ aria-label$="Total [[_patchChange.inserted]] lines added"
>
+[[_patchChange.inserted]]
</span>
<span
class="removed"
tabindex="0"
- aria-label$="[[_patchChange.deleted]] lines removed"
+ aria-label$="Total [[_patchChange.deleted]] lines removed"
>
-[[_patchChange.deleted]]
</span>
@@ -580,6 +693,12 @@
as="summaryEndpoint"
>
<gr-endpoint-decorator 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>
@@ -590,12 +709,18 @@
</div>
<div class="row totalChanges" hidden$="[[_hideBinaryChangeTotals]]">
<div class="total-stats">
- <span class="added" aria-label="Total lines added">
+ <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 lines removed">
+ <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)]]
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index f038f6c..ad0d1cf 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -95,6 +95,7 @@
});
stub('gr-diff-host', {
reload() { return Promise.resolve(); },
+ prefetchDiff() {},
});
// Element must be wrapped in an element with direct access to the
@@ -836,36 +837,42 @@
patchNum: '2',
};
element.$.fileCursor.setCursorAtIndex(0);
+ const reviewSpy = sandbox.spy(element, '_reviewFile');
+ const toggleExpandSpy = sandbox.spy(element, '_toggleFileExpanded');
flushAsynchronousOperations();
const fileRows =
dom(element.root).querySelectorAll('.row:not(.header-row)');
- const checkSelector = 'input.reviewed[type="checkbox"]';
+ const checkSelector = 'span.reviewedSwitch[role="switch"]';
const commitMsg = fileRows[0].querySelector(checkSelector);
const fileAdded = fileRows[1].querySelector(checkSelector);
const myFile = fileRows[2].querySelector(checkSelector);
- assert.isTrue(commitMsg.checked);
- assert.isFalse(fileAdded.checked);
- assert.isTrue(myFile.checked);
+ assert.equal(commitMsg.getAttribute('aria-checked'), 'true');
+ assert.equal(fileAdded.getAttribute('aria-checked'), 'false');
+ assert.equal(myFile.getAttribute('aria-checked'), 'true');
const commitReviewLabel = fileRows[0].querySelector('.reviewedLabel');
- const markReviewLabel = commitMsg.nextElementSibling;
+ const markReviewLabel = fileRows[0].querySelector('.markReviewed');
assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');
- const clickSpy = sandbox.spy(element, '_handleFileListClick');
+ const clickSpy = sandbox.spy(element, '_reviewedClick');
MockInteractions.tap(markReviewLabel);
- assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', false));
- assert.isFalse(commitReviewLabel.classList.contains('isReviewed'));
+ // assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', false));
+ // assert.isFalse(commitReviewLabel.classList.contains('isReviewed'));
assert.equal(markReviewLabel.textContent, 'MARK REVIEWED');
assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented);
+ assert.isTrue(reviewSpy.calledOnce);
MockInteractions.tap(markReviewLabel);
assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', true));
assert.isTrue(commitReviewLabel.classList.contains('isReviewed'));
assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED');
assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented);
+ assert.isTrue(reviewSpy.calledTwice);
+
+ assert.isFalse(toggleExpandSpy.called);
});
test('_computeFileStatusLabel', () => {
@@ -906,13 +913,6 @@
assert.isTrue(clickSpy.calledTwice);
assert.isTrue(toggleExpandSpy.calledOnce);
assert.isFalse(reviewStub.called);
-
- // Click the reviewed checkbox, resulting in a call to _reviewFile, but
- // no additional call to _toggleFileExpanded.
- row.querySelector('.markReviewed').click();
- assert.isTrue(clickSpy.calledThrice);
- assert.isTrue(toggleExpandSpy.calledOnce);
- assert.isTrue(reviewStub.calledOnce);
});
test('_handleFileListClick editMode', () => {
@@ -975,12 +975,12 @@
dom(element.root).querySelectorAll('.row:not(.header-row)');
// Because the label surrounds the input, the tap event is triggered
// there first.
- const showHideLabel = fileRows[0].querySelector('label.show-hide');
const showHideCheck = fileRows[0].querySelector(
- 'input.show-hide[type="checkbox"]');
- assert.isNotOk(showHideCheck.checked);
+ 'span.show-hide[role="switch"]');
+ const showHideLabel = showHideCheck.querySelector('.show-hide-icon');
+ assert.equal(showHideCheck.getAttribute('aria-checked'), 'false');
MockInteractions.tap(showHideLabel);
- assert.isOk(showHideCheck.checked);
+ assert.equal(showHideCheck.getAttribute('aria-checked'), 'true');
assert.notEqual(
element._expandedFiles.findIndex(f => f.path === 'myfile.txt'),
-1);
@@ -1001,7 +1001,7 @@
// Tap on a file to generate the diff.
const row = dom(element.root)
- .querySelectorAll('.row:not(.header-row) label.show-hide')[0];
+ .querySelectorAll('.row:not(.header-row) span.show-hide')[0];
MockInteractions.tap(row);
flushAsynchronousOperations();
@@ -1079,20 +1079,22 @@
const collapseStub = sandbox.stub(element, '_clearCollapsedDiffs');
const cursorUpdateStub = sandbox.stub(element.$.diffCursor,
'handleDiffUpdate');
+ const reInitStub = sandbox.stub(element.$.diffCursor,
+ 'reInitAndUpdateStops');
const path = 'path/to/my/file.txt';
element._filesByPath = {[path]: {}};
element.expandAllDiffs();
flushAsynchronousOperations();
assert.isTrue(element._showInlineDiffs);
- assert.isTrue(cursorUpdateStub.calledOnce);
+ assert.isTrue(reInitStub.calledOnce);
assert.equal(collapseStub.lastCall.args[0].length, 0);
element.collapseAllDiffs();
flushAsynchronousOperations();
assert.equal(element._expandedFiles.length, 0);
assert.isFalse(element._showInlineDiffs);
- assert.isTrue(cursorUpdateStub.calledTwice);
+ assert.isTrue(cursorUpdateStub.calledOnce);
assert.equal(collapseStub.lastCall.args[0].length, 1);
});
@@ -1105,6 +1107,7 @@
reload() {
done();
},
+ prefetchDiff() {},
cancel() {},
getCursorStops() { return []; },
addEventListener(eventName, callback) {
@@ -1162,6 +1165,7 @@
const diffs = [{
path: 'p0',
style: {},
+ prefetchDiff() {},
reload() {
assert.equal(callCount++, 2);
return Promise.resolve();
@@ -1169,6 +1173,7 @@
}, {
path: 'p1',
style: {},
+ prefetchDiff() {},
reload() {
assert.equal(callCount++, 1);
return Promise.resolve();
@@ -1176,6 +1181,7 @@
}, {
path: 'p2',
style: {},
+ prefetchDiff() {},
reload() {
assert.equal(callCount++, 0);
return Promise.resolve();
@@ -1198,6 +1204,7 @@
const diffs = [{
path: 'p0',
style: {},
+ prefetchDiff() {},
reload() {
assert.equal(reviewStub.callCount, 2);
assert.equal(callCount++, 2);
@@ -1206,6 +1213,7 @@
}, {
path: 'p1',
style: {},
+ prefetchDiff() {},
reload() {
assert.equal(reviewStub.callCount, 1);
assert.equal(callCount++, 1);
@@ -1214,6 +1222,7 @@
}, {
path: 'p2',
style: {},
+ prefetchDiff() {},
reload() {
assert.equal(reviewStub.callCount, 0);
assert.equal(callCount++, 0);
@@ -1236,6 +1245,7 @@
const diffs = [{
path: 'p',
style: {},
+ prefetchDiff() {},
reload() { return Promise.resolve(); },
}];
@@ -1565,6 +1575,7 @@
});
stub('gr-diff-host', {
reload() { return Promise.resolve(); },
+ prefetchDiff() {},
});
// Element must be wrapped in an element with direct access to the
@@ -1707,7 +1718,6 @@
test('n key with some files expanded and no shift key', () => {
MockInteractions.keyUpOn(fileRows[0], 73, null, 'i');
flushAsynchronousOperations();
- assert.equal(nextChunkStub.callCount, 1);
// Handle N key should return before calling diff cursor functions.
MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
@@ -1715,21 +1725,21 @@
assert.isFalse(nextCommentStub.called);
// This is also called in diffCursor.moveToFirstChunk.
- assert.equal(nextChunkStub.callCount, 2);
+ assert.equal(nextChunkStub.callCount, 1);
assert.equal(element.filesExpanded, 'some');
});
test('n key with some files expanded and shift key', () => {
MockInteractions.keyUpOn(fileRows[0], 73, null, 'i');
flushAsynchronousOperations();
- assert.equal(nextChunkStub.callCount, 1);
+ assert.equal(nextChunkStub.callCount, 0);
MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n');
assert.isTrue(nKeySpy.called);
assert.isTrue(nextCommentStub.called);
// This is also called in diffCursor.moveToFirstChunk.
- assert.equal(nextChunkStub.callCount, 1);
+ assert.equal(nextChunkStub.callCount, 0);
assert.equal(element.filesExpanded, 'some');
});
@@ -1742,7 +1752,7 @@
assert.isFalse(nextCommentStub.called);
// This is also called in diffCursor.moveToFirstChunk.
- assert.equal(nextChunkStub.callCount, 2);
+ assert.equal(nextChunkStub.callCount, 1);
assert.isTrue(element._showInlineDiffs);
});
@@ -1755,7 +1765,7 @@
assert.isTrue(nextCommentStub.called);
// This is also called in diffCursor.moveToFirstChunk.
- assert.equal(nextChunkStub.callCount, 1);
+ assert.equal(nextChunkStub.callCount, 0);
assert.isTrue(element._showInlineDiffs);
});
});
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js
index 2faac52..4670e6a 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js
@@ -67,7 +67,7 @@
}
</style>
<header>
- <h1 id="title">Included In:</h1>
+ <h1 id="title" class="heading-1">Included In:</h1>
<span class="closeButtonContainer">
<gr-button id="closeButton" link="" on-click="_handleCloseTap"
>Close</gr-button
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index be53652..e065008 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -35,7 +35,6 @@
const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+:\s*(.*)/;
const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?$/;
-const MSG_PREFIX = '#message-';
/**
* @extends PolymerElement
@@ -153,10 +152,6 @@
value: false,
},
_isCleanerLogExperimentEnabled: Boolean,
- _changeMessageUrl: {
- type: String,
- computed: '_computeChangeMessageUrl(message)',
- },
};
}
@@ -411,10 +406,13 @@
return classes.join(' ');
}
- _computeChangeMessageUrl(message) {
- if (!message) return '';
- const hash = MSG_PREFIX + message.id;
- return hash;
+ _handleAnchorClick(e) {
+ e.preventDefault();
+ this.dispatchEvent(new CustomEvent('message-anchor-tap', {
+ bubbles: true,
+ composed: true,
+ detail: {id: this.message.id},
+ }));
}
_handleReplyTap(e) {
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
index baaf5a3..de4a72a 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
@@ -181,9 +181,6 @@
content: 'PS ';
}
}
- .message-link {
- text-decoration: none;
- }
</style>
<div class$="[[_computeClass(_expanded)]]">
<div class="contentContainer">
@@ -303,20 +300,13 @@
</span>
</template>
<template is="dom-if" if="[[message.id]]">
- <span class="date">
+ <span class="date" on-click="_handleAnchorClick">
<gr-date-formatter
has-tooltip=""
show-date-and-time=""
date-str="[[message.date]]"
></gr-date-formatter>
</span>
- <a class="message-link" href="[[_changeMessageUrl]]">
- <iron-icon
- id="icon"
- class="link-icon"
- icon="gr-icons:link"
- ></iron-icon>
- </a>
</template>
<iron-icon
id="expandToggle"
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index 0bd53c7..332ca52 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -222,6 +222,26 @@
});
});
+ test('clicking on date link fires event', () => {
+ element.message = {
+ type: 'REVIEWER_UPDATE',
+ updated: '2016-01-12 20:24:49.448000000',
+ reviewer: {},
+ id: '47c43261_55aa2c41',
+ expanded: false,
+ };
+ flushAsynchronousOperations();
+ const stub = sinon.stub();
+ element.addEventListener('message-anchor-tap', stub);
+ const dateEl = element.shadowRoot
+ .querySelector('.date');
+ assert.ok(dateEl);
+ MockInteractions.tap(dateEl);
+
+ assert.isTrue(stub.called);
+ assert.deepEqual(stub.lastCall.args[0].detail, {id: element.message.id});
+ });
+
suite('compute messages', () => {
test('empty', () => {
assert.equal(element._computeMessageContent('', '', true), '');
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_html.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_html.js
index 5fd64d2..56cb960 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_html.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_html.js
@@ -111,6 +111,7 @@
filter="_isMessageVisible"
>
<gr-message
+ change="[[change]]"
change-num="[[changeNum]]"
message="[[message]]"
project-name="[[projectName]]"
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js
index 6aa7ffc..2321deb 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js
@@ -21,9 +21,6 @@
:host {
display: block;
}
- h3 {
- margin: var(--spacing-m) 0 0;
- }
section {
margin-bottom: 1.4em; /* Same as line height for collapse purposes */
}
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index b808bd9..e37c84a 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -295,6 +295,19 @@
this.addEventListener('comment-editing-changed', e => {
this._commentEditing = e.detail;
});
+
+ // Plugins on reply-reviewers endpoint can take advantage of these
+ // events to add / remove reviewers
+
+ this.addEventListener('add-reviewer', e => {
+ // Only support account type, see more from:
+ // elements/shared/gr-account-list/gr-account-list.js#addAccountItem
+ this.$.reviewers.addAccountItem({account: e.detail.reviewer});
+ });
+
+ this.addEventListener('remove-reviewer', e => {
+ this.$.reviewers.removeAccount(e.detail.reviewer);
+ });
}
/** @override */
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js
index 7286678..79f38a6 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js
@@ -97,20 +97,14 @@
}
.textareaContainer,
#textarea,
- gr-endpoint-decorator {
+ gr-endpoint-decorator[name='reply-text'] {
display: flex;
width: 100%;
}
- gr-endpoint-decorator[name='reply-label-scores'] {
- display: block;
- }
.previewContainer gr-formatted-text {
background: var(--table-header-background-color);
padding: var(--spacing-l);
}
- .draftsContainer h3 {
- margin-top: var(--spacing-xs);
- }
#checkingStatusLabel,
#notLatestLabel {
margin-left: var(--spacing-l);
@@ -143,20 +137,25 @@
</style>
<div class="container" tabindex="-1">
<section class="peopleContainer">
- <div class="peopleList">
- <div class="peopleListLabel">Reviewers</div>
- <gr-account-list
- id="reviewers"
- accounts="{{_reviewers}}"
- removable-values="[[change.removable_reviewers]]"
- filter="[[filterReviewerSuggestion]]"
- pending-confirmation="{{_reviewerPendingConfirmation}}"
- placeholder="Add reviewer..."
- on-account-text-changed="_handleAccountTextEntry"
- suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]"
- >
- </gr-account-list>
- </div>
+ <gr-endpoint-decorator name="reply-reviewers">
+ <gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
+ <div class="peopleList">
+ <div class="peopleListLabel">Reviewers</div>
+ <gr-account-list
+ id="reviewers"
+ accounts="{{_reviewers}}"
+ removable-values="[[change.removable_reviewers]]"
+ filter="[[filterReviewerSuggestion]]"
+ pending-confirmation="{{_reviewerPendingConfirmation}}"
+ placeholder="Add reviewer..."
+ on-account-text-changed="_handleAccountTextEntry"
+ suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]"
+ >
+ </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
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
index 989f97e..bd91990 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
@@ -25,6 +25,7 @@
import {parseDate} from '../../../utils/date-util.js';
import {NO_THREADS_MSG} from '../../../constants/messages.js';
+import {SpecialFilePath} from '../../../constants/constants.js';
/**
* Fired when a comment is saved or deleted
@@ -82,8 +83,69 @@
return loggedIn ? 'show' : '';
}
+ _compareThreads(c1, c2) {
+ if (c1.thread.path !== c2.thread.path) {
+ // '/PATCHSET' will not come before '/COMMIT' when sorting
+ // alphabetically so move it to the front explicitly
+ if (c1.thread.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
+ return -1;
+ }
+ if (c2.thread.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
+ return 1;
+ }
+ return c1.thread.path.localeCompare(c2.thread.path);
+ }
+
+ // Patchset comments have no line/range associated with them
+ if (c1.thread.line !== c2.thread.line) {
+ if (!c1.thread.line || !c2.thread.line) {
+ // one of them is a file level comment, show first
+ return c1.thread.line ? 1 : -1;
+ }
+ return c1.thread.line < c2.thread.line ? -1 : 1;
+ }
+
+ if (c1.thread.patchNum !== c2.thread.patchNum) {
+ return c1.thread.patchNum > c2.thread.patchNum ? -1 : 1;
+ }
+
+ if (c2.unresolved !== c1.unresolved) {
+ if (!c1.unresolved) { return 1; }
+ if (!c2.unresolved) { return -1; }
+ }
+
+ if (c2.hasDraft !== c1.hasDraft) {
+ if (!c1.hasDraft) { return 1; }
+ if (!c2.hasDraft) { return -1; }
+ }
+
+ const c1Date = c1.__date || parseDate(c1.updated);
+ const c2Date = c2.__date || parseDate(c2.updated);
+ const dateCompare = c2Date - c1Date;
+ if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) {
+ return 0;
+ }
+ return dateCompare ? dateCompare : c1.id.localeCompare(c2.id);
+ }
+
/**
* Observer on threads and update _sortedThreads when needed.
+ * Order as follows:
+ * - Patchset level threads (descending based on patchset number)
+ * - unresolved
+ - comments with drafts
+ - comments without drafts
+ * - resolved
+ - comments with drafts
+ - comments without drafts
+ * - File name
+ * - Line number
+ * - Unresolved (descending based on patchset number)
+ * - comments with drafts
+ * - comments without drafts
+ * - Resolved (descending based on patchset number)
+ * - comments with drafts
+ * - comments without drafts
*
* @param {Array<Object>} threads
* @param {!Object} spliceRecord
@@ -105,6 +167,7 @@
&& !isArrayMutation) {
// Instead of replacing the _sortedThreads which will trigger a re-render,
// we override all threads inside of it
+
for (const thread of threads) {
const idxInSortedThreads = this._sortedThreads
.findIndex(t => t.rootId === thread.rootId);
@@ -115,52 +178,38 @@
const threadsWithInfo = threads
.map(thread => this._getThreadWithStatusInfo(thread));
- this._sortedThreads = threadsWithInfo.sort((c1, c2) => {
- // threads will be sorted by:
- // - unresolved first
- // - with drafts
- // - file path
- // - line
- // - updated time
- if (c2.unresolved || c1.unresolved) {
- if (!c1.unresolved) { return 1; }
- if (!c2.unresolved) { return -1; }
- }
-
- if (c2.hasDraft || c1.hasDraft) {
- if (!c1.hasDraft) { return 1; }
- if (!c2.hasDraft) { return -1; }
- }
-
- // TODO: Update here once we introduce patchset level comments
- // they may not have or have a special line or path attribute
-
- if (c1.thread.path !== c2.thread.path) {
- return c1.thread.path.localeCompare(c2.thread.path);
- }
-
- // File level comments (no `line` property)
- // should always show before any lines
- if ([c1, c2].some(c => c.thread.line === undefined)) {
- if (!c1.thread.line) { return -1; }
- if (!c2.thread.line) { return 1; }
- } else if (c1.thread.line !== c2.thread.line) {
- return c1.thread.line - c2.thread.line;
- }
-
- const c1Date = c1.__date || parseDate(c1.updated);
- const c2Date = c2.__date || parseDate(c2.updated);
- const dateCompare = c2Date - c1Date;
- if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) {
- return 0;
- }
- return dateCompare ? dateCompare : c1.id.localeCompare(c2.id);
- }).map(threadInfo => threadInfo.thread);
+ this._sortedThreads = threadsWithInfo.sort((t1, t2) =>
+ this._compareThreads(t1, t2)).map(threadInfo => threadInfo.thread);
}
- _shouldShowThread(
- thread, unresolvedOnly, draftsOnly, onlyShowRobotCommentsWithHumanReply
- ) {
+ _isFirstThreadWithFileName(sortedThreads, thread, unresolvedOnly, draftsOnly,
+ onlyShowRobotCommentsWithHumanReply) {
+ const threads = sortedThreads.filter(t => this._shouldShowThread(
+ t, unresolvedOnly, draftsOnly,
+ onlyShowRobotCommentsWithHumanReply));
+ const index = threads.findIndex(t => t.rootId === thread.rootId);
+ if (index === -1) {
+ return false;
+ }
+ return index === 0 || (threads[index - 1].path !== threads[index].path);
+ }
+
+ _shouldRenderSeparator(sortedThreads, thread, unresolvedOnly, draftsOnly,
+ onlyShowRobotCommentsWithHumanReply) {
+ const threads = sortedThreads.filter(t => this._shouldShowThread(
+ t, unresolvedOnly, draftsOnly,
+ onlyShowRobotCommentsWithHumanReply));
+ const index = threads.findIndex(t => t.rootId === thread.rootId);
+ if (index === -1) {
+ return false;
+ }
+ return index > 0 && this._isFirstThreadWithFileName(sortedThreads,
+ thread, unresolvedOnly, draftsOnly,
+ onlyShowRobotCommentsWithHumanReply);
+ }
+
+ _shouldShowThread(thread, unresolvedOnly, draftsOnly,
+ onlyShowRobotCommentsWithHumanReply) {
if ([
thread,
unresolvedOnly,
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js
index 162a7c0..bb87db8 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js
@@ -53,6 +53,10 @@
.draftsOnly.unresolvedOnly gr-comment-thread[has-draft][unresolved] {
display: block;
}
+ .thread-separator {
+ border-top: 1px solid var(--border-color);
+ margin-top: var(--spacing-xl);
+ }
</style>
<template is="dom-if" if="[[!hideToggleButtons]]">
<div class="header">
@@ -85,11 +89,18 @@
is="dom-if"
if="[[_shouldShowThread(thread, _unresolvedOnly, _draftsOnly, onlyShowRobotCommentsWithHumanReply)]]"
>
+ <template
+ is="dom-if"
+ if="[[_shouldRenderSeparator(_sortedThreads, thread, _unresolvedOnly, _draftsOnly, onlyShowRobotCommentsWithHumanReply)]]"
+ >
+ <div class="thread-separator"></div>
+ </template>
<gr-comment-thread
show-file-path=""
change-num="[[changeNum]]"
comments="[[thread.comments]]"
comment-side="[[thread.commentSide]]"
+ show-file-name="[[_isFirstThreadWithFileName(_sortedThreads, thread, _unresolvedOnly, _draftsOnly, onlyShowRobotCommentsWithHumanReply)]]"
project-name="[[change.project]]"
is-on-parent="[[_isOnParent(thread.commentSide)]]"
line-num="[[thread.line]]"
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
index 7f3af7c..3b29ea7 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
@@ -36,6 +36,8 @@
import './gr-thread-list.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {NO_THREADS_MSG} from '../../../constants/messages.js';
+import {SpecialFilePath} from '../../../constants/constants.js';
+
suite('gr-thread-list tests', () => {
let element;
let sandbox;
@@ -123,7 +125,7 @@
id: '8caddf38_44770ec1',
updated: '2018-02-13 22:48:40.000000000',
message: 'Another unresolved comment',
- unresolved: true,
+ unresolved: false,
},
],
patchNum: 2,
@@ -178,6 +180,40 @@
{
comments: [
{
+ id: 'patchset_level_1',
+ path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
+ updated: '2018-02-15 22:48:48.018000000',
+ message: 'patchset comment 1',
+ unresolved: false,
+ __editing: false,
+ patch_set: '2',
+ },
+ ],
+ patchNum: 2,
+ path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
+ rootId: 'patchset_level_1',
+ start_datetime: '2018-02-09 18:49:18.000000000',
+ },
+ {
+ comments: [
+ {
+ id: 'patchset_level_2',
+ path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
+ updated: '2018-02-15 22:48:48.018000000',
+ message: 'patchset comment 2',
+ unresolved: false,
+ __editing: false,
+ patch_set: '3',
+ },
+ ],
+ patchNum: 3,
+ path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
+ rootId: 'patchset_level_2',
+ start_datetime: '2018-02-09 18:49:18.000000000',
+ },
+ {
+ comments: [
+ {
__path: '/COMMIT_MSG',
author: {
_account_id: 1000000,
@@ -267,6 +303,16 @@
assert.equal(getVisibleThreads().length, element.threads.length);
});
+ test('showing file name takes visible threads into account', () => {
+ assert.equal(element._isFirstThreadWithFileName(element._sortedThreads,
+ element._sortedThreads[2], element._unresolvedOnly, element._draftsOnly,
+ element.onlyShowRobotCommentsWithHumanReply), true);
+ element._unresolvedOnly = true;
+ assert.equal(element._isFirstThreadWithFileName(element._sortedThreads,
+ element._sortedThreads[2], element._unresolvedOnly, element._draftsOnly,
+ element.onlyShowRobotCommentsWithHumanReply), false);
+ });
+
test('onlyShowRobotCommentsWithHumanReply ', () => {
element.onlyShowRobotCommentsWithHumanReply = true;
flushAsynchronousOperations();
@@ -276,29 +322,153 @@
assert.isNotOk(getVisibleThreads().find(th => th.rootId === 'rc1'));
});
+ suite('_compareThreads', () => {
+ test('patchset comes before any other file', () => {
+ const t1 = {thread: {path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS}};
+ const t2 = {thread: {path: SpecialFilePath.COMMIT_MESSAGE}};
+
+ t1.patchNum = t2.patchNum = 1;
+ t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
+ assert.equal(element._compareThreads(t1, t2), -1);
+ assert.equal(element._compareThreads(t2, t1), 1);
+
+ // assigning values to properties such that t2 should come first
+ t1.patchNum = 1;
+ t2.patchNum = 2;
+ t1.unresolved = t1.hasDraft = false;
+ t2.unresolved = t2.unresolved = true;
+ assert.equal(element._compareThreads(t1, t2), -1);
+ assert.equal(element._compareThreads(t2, t1), 1);
+ });
+
+ test('file path is compared lexicographically', () => {
+ const t1 = {thread: {path: 'a.txt'}};
+ const t2 = {thread: {path: 'b.txt'}};
+ t1.patchNum = t2.patchNum = 1;
+ t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
+ assert.equal(element._compareThreads(t1, t2), -1);
+ assert.equal(element._compareThreads(t2, t1), 1);
+
+ t1.patchNum = 1;
+ t2.patchNum = 2;
+ t1.unresolved = t1.hasDraft = false;
+ t2.unresolved = t2.unresolved = true;
+ assert.equal(element._compareThreads(t1, t2), -1);
+ assert.equal(element._compareThreads(t2, t1), 1);
+ });
+
+ test('patchset comments sorted by reverse patchset', () => {
+ const t1 = {thread: {path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
+ patchNum: 1}};
+ const t2 = {thread: {path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
+ patchNum: 2}};
+ t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
+ assert.equal(element._compareThreads(t1, t2), 1);
+ assert.equal(element._compareThreads(t2, t1), -1);
+
+ t1.unresolved = t1.hasDraft = false;
+ t2.unresolved = t2.unresolved = true;
+ assert.equal(element._compareThreads(t1, t2), 1);
+ assert.equal(element._compareThreads(t2, t1), -1);
+ });
+
+ test('patchset comments with same patchset picks unresolved first', () => {
+ const t1 = {thread: {path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
+ patchNum: 1}, unresolved: true};
+ const t2 = {thread: {path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
+ patchNum: 1}, unresolved: false};
+ t1.hasDraft = t2.hasDraft = false;
+ assert.equal(element._compareThreads(t1, t2), -1);
+ assert.equal(element._compareThreads(t2, t1), 1);
+ });
+
+ test('file level comment before line', () => {
+ const t1 = {thread: {path: 'a.txt', line: 2}};
+ const t2 = {thread: {path: 'a.txt'}};
+ t1.patchNum = t2.patchNum = 1;
+ t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
+ assert.equal(element._compareThreads(t1, t2), 1);
+ assert.equal(element._compareThreads(t2, t1), -1);
+
+ // give preference to t1 in unresolved/draft properties
+ t1.unresolved = t1.hasDraft = true;
+ t2.unresolved = t2.unresolved = false;
+ assert.equal(element._compareThreads(t1, t2), 1);
+ assert.equal(element._compareThreads(t2, t1), -1);
+ });
+
+ test('comments sorted by line', () => {
+ const t1 = {thread: {path: 'a.txt', line: 2}};
+ const t2 = {thread: {path: 'a.txt', line: 3}};
+ t1.patchNum = t2.patchNum = 1;
+ t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
+ assert.equal(element._compareThreads(t1, t2), -1);
+ assert.equal(element._compareThreads(t2, t1), 1);
+
+ t1.unresolved = t1.hasDraft = false;
+ t2.unresolved = t2.unresolved = true;
+ assert.equal(element._compareThreads(t1, t2), -1);
+ assert.equal(element._compareThreads(t2, t1), 1);
+ });
+
+ test('comments on same line sorted by reverse patchset', () => {
+ const t1 = {thread: {path: 'a.txt', line: 2, patchNum: 1}};
+ const t2 = {thread: {path: 'a.txt', line: 2, patchNum: 2}};
+ t1.unresolved = t2.unresolved = t1.hasDraft = t2.hasDraft = false;
+ assert.equal(element._compareThreads(t1, t2), 1);
+ assert.equal(element._compareThreads(t2, t1), -1);
+
+ // give preference to t1 in unresolved/draft properties
+ t1.unresolved = t1.hasDraft = true;
+ t2.unresolved = t2.unresolved = false;
+ assert.equal(element._compareThreads(t1, t2), 1);
+ assert.equal(element._compareThreads(t2, t1), -1);
+ });
+
+ test('comments on same line & patchset sorted by unresolved first',
+ () => {
+ const t1 = {thread: {path: 'a.txt', line: 2, patchNum: 1},
+ unresolved: true};
+ const t2 = {thread: {path: 'a.txt', line: 2, patchNum: 1},
+ unresolved: false};
+ t1.patchNum = t2.patchNum = 1;
+ assert.equal(element._compareThreads(t1, t2), -1);
+ assert.equal(element._compareThreads(t2, t1), 1);
+
+ t2.hasDraft = true;
+ t1.hasDraft = false;
+ assert.equal(element._compareThreads(t1, t2), -1);
+ assert.equal(element._compareThreads(t2, t1), 1);
+ });
+
+ test('comments on same line & patchset & unresolved sorted by draft',
+ () => {
+ const t1 = {thread: {path: 'a.txt', line: 2, patchNum: 1},
+ unresolved: true, hasDraft: false};
+ const t2 = {thread: {path: 'a.txt', line: 2, patchNum: 1},
+ unresolved: true, hasDraft: true};
+ t1.patchNum = t2.patchNum = 1;
+ assert.equal(element._compareThreads(t1, t2), 1);
+ assert.equal(element._compareThreads(t2, t1), -1);
+ });
+ });
+
test('_computeSortedThreads', () => {
- assert.equal(element._sortedThreads.length, 7);
- // Draft and unresolved for commit-msg at line 5
- assert.equal(element._sortedThreads[0].rootId,
- 'ecf0b9fa_fe1a5f62');
- // unresolved no draft and file level
- assert.equal(element._sortedThreads[1].rootId,
- '8caddf38_44770ec1');
- // unresolved no draft at line 4
- assert.equal(element._sortedThreads[2].rootId,
- 'scaddf38_44770ec1');
- // unresolved no draft at line 5
- assert.equal(element._sortedThreads[3].rootId,
- 'rc1');
- // Unresolved no draft at line 7
- assert.equal(element._sortedThreads[4].rootId,
- 'rc2');
- // resolved and draft on COMMIT_MSG
- assert.equal(element._sortedThreads[5].rootId,
- 'zcf0b9fa_fe1a5f62');
- // resolved and on file test.txt
- assert.equal(element._sortedThreads[6].rootId,
- '09a9fb0a_1484e6cf');
+ assert.equal(element._sortedThreads.length, 9);
+ const expectedSortedRootIds = [
+ 'patchset_level_2', // Posted on Patchset 3
+ 'patchset_level_1', // Posted on Patchset 2
+ '8caddf38_44770ec1', // File level on COMMIT_MSG
+ 'scaddf38_44770ec1', // Line 4 on COMMIT_MSG
+ 'ecf0b9fa_fe1a5f62', // Line 5 on COMMIT_MESSAGE but with drafts
+ 'rc1', // Line 5 on COMMIT_MESSAGE without drafts
+ 'zcf0b9fa_fe1a5f62', // Line 6 on COMMIT_MSG
+ 'rc2', // Line 7 on COMMIT_MSG
+ '09a9fb0a_1484e6cf', // File level on test.txt
+ ];
+ element._sortedThreads.forEach((thread, index) => {
+ assert.equal(thread.rootId, expectedSortedRootIds[index]);
+ });
});
test('thread removal and sort again', () => {
@@ -308,24 +478,20 @@
composed: true, bubbles: true,
}));
flushAsynchronousOperations();
- assert.equal(element._sortedThreads.length, 6);
- assert.equal(element._sortedThreads[0].rootId,
- 'ecf0b9fa_fe1a5f62');
- // unresolved no draft and file level
- assert.equal(element._sortedThreads[1].rootId,
- '8caddf38_44770ec1');
- // unresolved no draft at line 4
- assert.equal(element._sortedThreads[2].rootId,
- 'scaddf38_44770ec1');
- // unresolved no draft at line 5
- assert.equal(element._sortedThreads[3].rootId,
- 'rc1');
- // resolved and draft
- assert.equal(element._sortedThreads[4].rootId,
- 'zcf0b9fa_fe1a5f62');
- // resolved and on file test.txt
- assert.equal(element._sortedThreads[5].rootId,
- '09a9fb0a_1484e6cf');
+ assert.equal(element._sortedThreads.length, 8);
+ const expectedSortedRootIds = [
+ 'patchset_level_2',
+ 'patchset_level_1',
+ '8caddf38_44770ec1', // File level on COMMIT_MSG
+ 'scaddf38_44770ec1', // Line 4 on COMMIT_MSG
+ 'ecf0b9fa_fe1a5f62', // Line 5 on COMMIT_MESSAGE but with drafts
+ 'rc1', // Line 5 on COMMIT_MESSAGE without drafts
+ 'zcf0b9fa_fe1a5f62', // Line 6 on COMMIT_MSG
+ '09a9fb0a_1484e6cf', // File level on test.txt
+ ];
+ element._sortedThreads.forEach((thread, index) => {
+ assert.equal(thread.rootId, expectedSortedRootIds[index]);
+ });
});
test('modification on thread shold not trigger sort again', () => {
@@ -343,28 +509,20 @@
assert.notDeepEqual(currentSortedThreads, element._sortedThreads);
// exact same order as in _computeSortedThreads
- assert.equal(element._sortedThreads.length, 7);
- // Draft and unresolved for commit-msg at line 5
- assert.equal(element._sortedThreads[0].rootId,
- 'ecf0b9fa_fe1a5f62');
- // unresolved no draft and file level
- assert.equal(element._sortedThreads[1].rootId,
- '8caddf38_44770ec1');
- // unresolved no draft at line 4
- assert.equal(element._sortedThreads[2].rootId,
- 'scaddf38_44770ec1');
- // unresolved no draft at line 5
- assert.equal(element._sortedThreads[3].rootId,
- 'rc1');
- // Unresolved no draft at line 7
- assert.equal(element._sortedThreads[4].rootId,
- 'rc2');
- // resolved and draft on COMMIT_MSG
- assert.equal(element._sortedThreads[5].rootId,
- 'zcf0b9fa_fe1a5f62');
- // resolved and on file test.txt
- assert.equal(element._sortedThreads[6].rootId,
- '09a9fb0a_1484e6cf');
+ const expectedSortedRootIds = [
+ 'patchset_level_2',
+ 'patchset_level_1',
+ '8caddf38_44770ec1', // File level on COMMIT_MSG
+ 'scaddf38_44770ec1', // Line 4 on COMMIT_MSG
+ 'ecf0b9fa_fe1a5f62', // Line 5 on COMMIT_MESSAGE but with drafts
+ 'rc1', // Line 5 on COMMIT_MESSAGE without drafts
+ 'zcf0b9fa_fe1a5f62', // Line 6 on COMMIT_MSG
+ 'rc2', // Line 7 on COMMIT_MSG
+ '09a9fb0a_1484e6cf', // File level on test.txt
+ ];
+ element._sortedThreads.forEach((thread, index) => {
+ assert.equal(thread.rootId, expectedSortedRootIds[index]);
+ });
});
test('reset sortedThreads when threads set to undefiend', () => {
@@ -373,7 +531,7 @@
});
test('non-equal length of sortThreads and threads' +
- ' shold trigger sort again', () => {
+ ' should trigger sort again', () => {
const modifiedThreads = [...element.threads];
const currentSortedThreads = [...element._sortedThreads];
element._sortedThreads = [];
@@ -381,36 +539,27 @@
assert.deepEqual(currentSortedThreads, element._sortedThreads);
// exact same order as in _computeSortedThreads
- assert.equal(element._sortedThreads.length, 7);
- // Draft and unresolved for commit-msg at line 5
- assert.equal(element._sortedThreads[0].rootId,
- 'ecf0b9fa_fe1a5f62');
- // /COMMIT_MSG
- // unresolved no draft and file level
- assert.equal(element._sortedThreads[1].rootId,
- '8caddf38_44770ec1');
- // unresolved no draft at line 4
- assert.equal(element._sortedThreads[2].rootId,
- 'scaddf38_44770ec1');
- // unresolved no draft at line 5
- assert.equal(element._sortedThreads[3].rootId,
- 'rc1');
- // Unresolved no draft at line 7
- assert.equal(element._sortedThreads[4].rootId,
- 'rc2');
- // resolved and draft on COMMIT_MSG
- assert.equal(element._sortedThreads[5].rootId,
- 'zcf0b9fa_fe1a5f62');
- // resolved and on file test.txt
- assert.equal(element._sortedThreads[6].rootId,
- '09a9fb0a_1484e6cf');
+ const expectedSortedRootIds = [
+ 'patchset_level_2',
+ 'patchset_level_1',
+ '8caddf38_44770ec1', // File level on COMMIT_MSG
+ 'scaddf38_44770ec1', // Line 4 on COMMIT_MSG
+ 'ecf0b9fa_fe1a5f62', // Line 5 on COMMIT_MESSAGE but with drafts
+ 'rc1', // Line 5 on COMMIT_MESSAGE without drafts
+ 'zcf0b9fa_fe1a5f62', // Line 6 on COMMIT_MSG
+ 'rc2', // Line 7 on COMMIT_MSG
+ '09a9fb0a_1484e6cf', // File level on test.txt
+ ];
+ element._sortedThreads.forEach((thread, index) => {
+ assert.equal(thread.rootId, expectedSortedRootIds[index]);
+ });
});
test('toggle unresolved only shows unresolved comments', () => {
MockInteractions.tap(element.shadowRoot.querySelector(
'#unresolvedToggle'));
flushAsynchronousOperations();
- assert.equal(getVisibleThreads().length, 5);
+ assert.equal(getVisibleThreads().length, 4);
});
test('toggle drafts only shows threads with draft comments', () => {
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
index 903552a..23a4c55 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -90,7 +90,9 @@
return undefined;
}
- const links = [{name: 'Settings', url: '/settings/'}];
+ const links = [];
+ links.push({name: 'Settings', url: '/settings/'});
+ links.push({name: 'Keyboard Shortcuts', id: 'shortcuts'});
if (switchAccountUrl) {
const replacements = {path};
const url = this._interpolateUrl(switchAccountUrl, replacements);
@@ -107,6 +109,11 @@
];
}
+ _handleShortcutsTap(e) {
+ this.dispatchEvent(new CustomEvent('show-keyboard-shortcuts',
+ {bubbles: true, composed: true}));
+ }
+
_handleLocationChange() {
this._path =
window.location.pathname +
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js
index b47894e..5db7923 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js
@@ -37,6 +37,7 @@
link=""
items="[[links]]"
top-content="[[topContent]]"
+ on-tap-item-shortcuts="_handleShortcutsTap"
horizontal-align="right"
>
<span hidden$="[[_hasAvatars]]" hidden="">[[_accountName(account)]]</span>
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
index 6c8ed68..a366d09 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
@@ -84,12 +84,12 @@
assert.isUndefined(element._getLinks(null));
// No switch account link.
- assert.equal(element._getLinks(null, '').length, 2);
+ assert.equal(element._getLinks(null, '').length, 3);
// Unparameterized switch account link.
let links = element._getLinks('/switch-account', '');
- assert.equal(links.length, 3);
- assert.deepEqual(links[1], {
+ assert.equal(links.length, 4);
+ assert.deepEqual(links[2], {
name: 'Switch account',
url: '/switch-account',
external: true,
@@ -97,8 +97,8 @@
// Parameterized switch account link.
links = element._getLinks('/switch-account${path}', '/c/123');
- assert.equal(links.length, 3);
- assert.deepEqual(links[1], {
+ assert.equal(links.length, 4);
+ assert.deepEqual(links[2], {
name: 'Switch account',
url: '/switch-account/c/123',
external: true,
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js
index 78b576e..e850fbe 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js
@@ -55,7 +55,7 @@
}
</style>
<header>
- <h3>Keyboard shortcuts</h3>
+ <h3 class="heading-3">Keyboard shortcuts</h3>
<gr-button link="" on-click="_handleCloseTap">Close</gr-button>
</header>
<main>
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
index 8308942..69e2989 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
@@ -393,13 +393,12 @@
* @param {number=} opt_patchNum
* @return {string}
*/
- getUrlForChangeById(changeNum, project, opt_patchNum, opt_messageHash) {
+ getUrlForChangeById(changeNum, project, opt_patchNum) {
return this._getUrlFor({
view: GerritNav.View.CHANGE,
changeNum,
project,
patchNum: opt_patchNum,
- messageHash: opt_messageHash,
});
},
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
index 1c4edec..c9d5d78 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
@@ -211,8 +211,8 @@
null;
}
- getContentByLine(lineNumber, opt_side, opt_root) {
- return this._builder.getContentByLine(lineNumber, opt_side, opt_root);
+ getContentTdByLine(lineNumber, opt_side, opt_root) {
+ return this._builder.getContentTdByLine(lineNumber, opt_side, opt_root);
}
_getDiffRowByChild(child) {
@@ -222,14 +222,14 @@
return child;
}
- getContentByLineEl(lineEl) {
+ getContentTdByLineEl(lineEl) {
if (!lineEl) return;
const line = lineEl.getAttribute('data-value');
const side = this.getSideByLineEl(lineEl);
// Performance optimization because we already have an element in the
// correct row
const row = dom(this._getDiffRowByChild(lineEl));
- return this.getContentByLine(line, side, row);
+ return this.getContentTdByLine(line, side, row);
}
getLineElByNumber(lineNumber, opt_side) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
index 67418e2..057401b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
@@ -973,22 +973,24 @@
assert.equal(actual.textContent, diff.content[1].b[0]);
});
- test('getContentByLineEl works both with button and td', () => {
+ 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 contentLeft = diffRow.querySelectorAll('.contentText')[0];
+ const contentTdLeft = diffRow.querySelectorAll('.content')[0];
const lineNumTdRight = diffRow.querySelector('td.lineNum.right');
const lineNumButtonRight = lineNumTdRight.querySelector('button');
- const contentRight = diffRow.querySelectorAll('.contentText')[1];
+ const contentTdRight = diffRow.querySelectorAll('.content')[1];
- assert.equal(element.getContentByLineEl(lineNumTdLeft), contentLeft);
- assert.equal(element.getContentByLineEl(lineNumButtonLeft), contentLeft);
- assert.equal(element.getContentByLineEl(lineNumTdRight), contentRight);
+ assert.equal(element.getContentTdByLineEl(lineNumTdLeft), contentTdLeft);
assert.equal(
- element.getContentByLineEl(lineNumButtonRight), contentRight);
+ element.getContentTdByLineEl(lineNumButtonLeft), contentTdLeft);
+ assert.equal(
+ element.getContentTdByLineEl(lineNumTdRight), contentTdRight);
+ assert.equal(
+ element.getContentTdByLineEl(lineNumButtonRight), contentTdRight);
});
test('findLinesByRange', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index 74a4591..1cabd82 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -146,12 +146,18 @@
return groups;
};
-GrDiffBuilder.prototype.getContentByLine = function(lineNumber, opt_side,
- opt_root) {
+GrDiffBuilder.prototype.getContentTdByLine = function(
+ lineNumber, opt_side, opt_root) {
const root = dom(opt_root || this._outputEl);
const sideSelector = opt_side ? ('.' + opt_side) : '';
return root.querySelector('td.lineNum[data-value="' + lineNumber +
- '"]' + sideSelector + ' ~ td.content .contentText');
+ '"]' + sideSelector + ' ~ td.content');
+};
+
+GrDiffBuilder.prototype.getContentByLine = function(
+ lineNumber, opt_side, opt_root) {
+ return this.getContentTdByLine(lineNumber, opt_side, opt_root)
+ .querySelector('.contentText');
};
/**
@@ -309,15 +315,20 @@
}
if (line.type === GrDiffLine.Type.BOTH || line.type === type) {
- const button = this._createElement('button');
- button.tabIndex = -1;
- td.appendChild(button);
-
// Both td and button need a number of classes/attributes for various
// selectors to work.
this._decorateLineEl(td, number, side);
td.classList.add('lineNum');
+
+ if (this._prefs.show_file_comment_button === false && number === 'FILE') {
+ return td;
+ }
+
+ const button = this._createElement('button');
+ td.appendChild(button);
+ button.tabIndex = -1;
this._decorateLineEl(button, number, side);
+
button.classList.add('lineNumButton');
button.textContent = number === 'FILE' ? 'File' : number;
@@ -357,22 +368,26 @@
}
td.classList.add(line.type);
- const lineLimit =
- !this._prefs.line_wrapping ? this._prefs.line_length : Infinity;
+ if (line.beforeNumber !== 'FILE') {
+ const lineLimit =
+ !this._prefs.line_wrapping ? this._prefs.line_length : Infinity;
+ const contentText =
+ this._formatText(line.text, this._prefs.tab_size, lineLimit);
- const contentText =
- this._formatText(line.text, this._prefs.tab_size, lineLimit);
- if (opt_side) {
- contentText.setAttribute('data-side', opt_side);
- }
-
- for (const layer of this.layers) {
- if (typeof layer.annotate == 'function') {
- layer.annotate(contentText, lineNumberEl, line);
+ if (opt_side) {
+ contentText.setAttribute('data-side', opt_side);
}
- }
- td.appendChild(contentText);
+ for (const layer of this.layers) {
+ if (typeof layer.annotate == 'function') {
+ layer.annotate(contentText, lineNumberEl, line);
+ }
+ }
+
+ td.appendChild(contentText);
+ } else {
+ td.classList.add('file');
+ }
return td;
};
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
index c5dab43..229ae43 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -290,11 +290,8 @@
if (!line) {
return;
}
- const contentText = this.diffBuilder.getContentByLineEl(lineEl);
- if (!contentText) {
- return;
- }
- const contentTd = contentText.parentElement;
+ const contentTd = this.diffBuilder.getContentTdByLineEl(lineEl);
+ const contentText = contentTd.querySelector('.contentText');
if (!contentTd.contains(node)) {
node = contentText;
column = 0;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
index 86f1505..ec7e6d2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
@@ -267,9 +267,9 @@
contentTd,
contentText,
});
- builder.getContentByLineEl.withArgs(lineEl).returns(contentText);
+ builder.getContentTdByLineEl.withArgs(lineEl).returns(contentTd);
builder.getLineNumberByChild.withArgs(lineEl).returns(line);
- builder.getContentByLine.withArgs(line, side).returns(contentText);
+ builder.getContentTdByLine.withArgs(line, side).returns(contentTd);
builder.getSideByLineEl.withArgs(lineEl).returns(side);
return contentText;
};
@@ -296,8 +296,8 @@
});
diff = element.querySelector('#diffTable');
builder = {
- getContentByLine: sandbox.stub(),
- getContentByLineEl: sandbox.stub(),
+ getContentTdByLine: sandbox.stub(),
+ getContentTdByLineEl: sandbox.stub(),
getLineElByChild,
getLineNumberByChild: sandbox.stub(),
getSideByLineEl: sandbox.stub(),
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
index b367265..cb299de 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
@@ -218,6 +218,11 @@
notify: true,
},
+ _fetchDiffPromise: {
+ type: Object,
+ value: null,
+ },
+
/** @type {?Object} */
_blame: {
type: Object,
@@ -367,7 +372,7 @@
event.detail.contentRendered;
if (needsSyntaxHighlighting) {
this.reporting.time(TimingLabel.SYNTAX);
- this.$.syntaxLayer.process().then(() => {
+ this.$.syntaxLayer.process().finally(() => {
this.reporting.timeEnd(TimingLabel.SYNTAX);
this.reporting.timeEnd(TimingLabel.TOTAL);
resolve();
@@ -454,6 +459,7 @@
/** Cancel any remaining diff builder rendering work. */
cancel() {
this.$.diff.cancel();
+ this.$.syntaxLayer.cancel();
}
/** @return {!Array<!HTMLElement>} */
@@ -534,8 +540,21 @@
!this.noAutoRender;
}
+ // TODO(milutin): Use rest-api with fetchCacheURL instead of this.
+ prefetchDiff() {
+ if (!!this.changeNum && !!this.patchRange && !!this.path
+ && this._fetchDiffPromise === null) {
+ this._fetchDiffPromise = this._getDiff();
+ }
+ }
+
/** @return {!Promise<!Object>} */
_getDiff() {
+ if (this._fetchDiffPromise !== null) {
+ const fetchDiffPromise = this._fetchDiffPromise;
+ this._fetchDiffPromise = null;
+ return fetchDiffPromise;
+ }
// Wrap the diff request in a new promise so that the error handler
// rejects the promise, allowing the error to be handled in the .catch.
return new Promise((resolve, reject) => {
@@ -792,6 +811,7 @@
}
threadEl.changeNum = this.changeNum;
threadEl.patchNum = thread.patchNum;
+ threadEl.showPatchset = false;
threadEl.lineNum = thread.lineNum;
const rootIdChangedListener = changeEvent => {
thread.rootId = changeEvent.detail.value;
@@ -905,6 +925,7 @@
return;
}
+ this._fetchDiffPromise = null;
if (preferredWhitespaceLevel !== loadedWhitespaceLevel &&
!noRenderOnPrefsChange) {
this.reload();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
index b8f26b2..342c7ec 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
@@ -445,6 +445,19 @@
});
});
+ test('prefetch getDiff', done => {
+ const diffRestApiStub = sandbox.stub(element.$.restAPI, 'getDiff')
+ .returns(Promise.resolve({content: []}));
+ element.changeNum = 123;
+ element.patchRange = {basePatchNum: 1, patchNum: 2};
+ element.path = 'file.txt';
+ element.prefetchDiff();
+ element._getDiff().then(() =>{
+ assert.isTrue(diffRestApiStub.calledOnce);
+ done();
+ });
+ });
+
test('_getDiff handles null diff responses', done => {
stub('gr-rest-api-interface', {
getDiff() { return Promise.resolve(null); },
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
index 608e898..cfbe481 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -24,7 +24,7 @@
import {htmlTemplate} from './gr-diff-selection_html.js';
import {DomUtilBehavior} from '../../../behaviors/dom-util-behavior/dom-util-behavior.js';
import {GrRangeNormalizer} from '../gr-diff-highlight/gr-range-normalizer.js';
-import {util} from '../../../scripts/util.js';
+import {querySelectorAll} from '../../../utils/dom-util.js';
/**
* Possible CSS classes indicating the state of selection. Dynamically added/
@@ -203,7 +203,7 @@
}
_getSelection() {
- const diffHosts = util.querySelectorAll(document.body, 'gr-diff');
+ const diffHosts = querySelectorAll(document.body, 'gr-diff');
if (!diffHosts.length) return window.getSelection();
const curDiffHost = diffHosts.find(diffHost => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 415ce1e..19abdaf 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -298,6 +298,8 @@
[this.Shortcut.EXPAND_ALL_DIFF_CONTEXT]: '_handleExpandAllDiffContext',
[this.Shortcut.NEXT_UNREVIEWED_FILE]: '_handleNextUnreviewedFile',
[this.Shortcut.TOGGLE_BLAME]: '_handleToggleBlame',
+ [this.Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS]:
+ '_handleToggleHideAllCommentThreads',
// Final two are actually handled by gr-comment-thread.
[this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
@@ -740,6 +742,7 @@
this._initCursor(this.params);
this._changeNum = value.changeNum;
+ this.classList.remove('hideComments');
this._path = value.path;
this._patchRange = {
patchNum: value.patchNum,
@@ -798,6 +801,8 @@
promises.push(this._getChangeEdit(this._changeNum));
+ this.$.diffHost.cancel();
+ this.$.diffHost.clearDiffContent();
this._loading = true;
return Promise.all(promises)
.then(r => {
@@ -1275,6 +1280,12 @@
this._toggleBlame();
}
+ _handleToggleHideAllCommentThreads(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+ this.toggleClass('hideComments');
+ }
+
_computeBlameLoaderClass(isImageDiff, path) {
return !this.isMagicPath(path) && !isImageDiff ? 'show' : '';
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js
index e735153..3d30f77 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js
@@ -196,6 +196,9 @@
}
}
}
+ :host(.hideComments) {
+ --gr-comment-thread-display: none;
+ }
</style>
<gr-fixed-panel
class$="[[_computeContainerClass(_editMode)]]"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index 98371dc..21d1144 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -71,6 +71,7 @@
kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
kb.bindShortcut(kb.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
kb.bindShortcut(kb.Shortcut.EXPAND_ALL_COMMENT_THREADS, 'e');
+ kb.bindShortcut(kb.Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, 'h');
kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_COMMENT_THREADS, 'shift+e');
kb.bindShortcut(kb.Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m');
kb.bindShortcut(kb.Shortcut.TOGGLE_BLAME, 'b');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index bfe7325..0942646 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -433,7 +433,8 @@
}
return Array.from(
- dom(this.root).querySelectorAll(':not(.contextControl) > .diff-row'));
+ dom(this.root).querySelectorAll(':not(.contextControl) > .diff-row'))
+ .filter(tr => tr.querySelector('button'));
}
/** @return {boolean} */
@@ -592,11 +593,7 @@
* @param {!Object=} range
*/
_createComment(lineEl, lineNum, side, range) {
- const contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
- if (!contentText) {
- return;
- }
- const contentEl = contentText.parentElement;
+ const contentEl = this.$.diffBuilder.getContentTdByLineEl(lineEl);
side = side ||
this._getCommentSideByLineAndContent(lineEl, contentEl);
const patchForNewThreads = this._getPatchNumByLineAndContent(
@@ -831,11 +828,7 @@
const commentSide = threadEl.getAttribute('comment-side');
const lineEl = this.$.diffBuilder.getLineElByNumber(
lineNumString, commentSide);
- const contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
- if (!contentText) {
- continue;
- }
- const contentEl = contentText.parentElement;
+ const contentEl = this.$.diffBuilder.getContentTdByLineEl(lineEl);
const threadGroupEl = this._getOrCreateThreadGroup(
contentEl, commentSide);
// Create a slot for the thread and attach it to the thread group.
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js
index 55abd38..279f968 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js
@@ -90,6 +90,16 @@
.content {
background-color: var(--diff-blank-background-color);
}
+ /*
+ The file line, which has no contentText, add some margin before the first
+ comment. We cannot add padding the container because we only want it if
+ there is at least one comment thread, and the slotting makes :empty not
+ work as expected.
+ */
+ .content.file slot:first-child::slotted(.comment-thread) {
+ display: block;
+ margin-top: var(--spacing-xs);
+ }
.contentText {
background-color: var(--view-background-color);
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
index d497b9a..a10db97 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
@@ -21,7 +21,7 @@
import './gr-diff.js';
import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image.js';
-import {util} from '../../../scripts/util.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';
@@ -82,14 +82,14 @@
element = basicFixture.instantiate();
element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: true});
flushAsynchronousOperations();
- assert.equal(util.getComputedStyleValue('--line-limit', element), '80ch');
+ assert.equal(getComputedStyleValue('--line-limit', element), '80ch');
});
test('line limit without line_wrapping', () => {
element = basicFixture.instantiate();
element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: false});
flushAsynchronousOperations();
- assert.isNotOk(util.getComputedStyleValue('--line-limit', element));
+ assert.isNotOk(getComputedStyleValue('--line-limit', element));
});
suite('_get{PatchNum|IsParentComment}ByLineAndContent', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
index 3bf3697..c00c0fb 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -25,6 +25,7 @@
import {htmlTemplate} from './gr-patch-range-select_html.js';
import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
+import {appContext} from '../../../services/app-context.js';
// Maximum length for patch set descriptions.
const PATCH_DESC_MAX_LENGTH = 500;
@@ -78,6 +79,11 @@
];
}
+ constructor() {
+ super();
+ this.reporting = appContext.reportingService;
+ }
+
_getShaForPatch(patch) {
return patch.sha.substring(0, 10);
}
@@ -285,8 +291,14 @@
const target = dom(e).localTarget;
if (target === this.$.patchNumDropdown) {
+ if (detail.patchNum === e.detail.value) return;
+ this.reporting.reportInteraction('right-patchset-changed',
+ {previous: detail.patchNum, current: e.detail.value});
detail.patchNum = e.detail.value;
} else {
+ if (detail.basePatchNum === e.detail.value) return;
+ this.reporting.reportInteraction('left-patchset-changed',
+ {previous: detail.basePatchNum, current: e.detail.value});
detail.basePatchNum = e.detail.value;
}
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
index ac5804f..1a0bbd9 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -249,7 +249,7 @@
process() {
// Cancel any still running process() calls, because they append to the
// same _baseRanges and _revisionRanges fields.
- this._cancel();
+ this.cancel();
// Discard existing ranges.
this._baseRanges = [];
@@ -319,7 +319,7 @@
/**
* Cancel any asynchronous syntax processing jobs.
*/
- _cancel() {
+ cancel() {
if (this._processHandle != null) {
this.cancelAsync(this._processHandle);
this._processHandle = null;
@@ -330,7 +330,7 @@
}
_diffChanged() {
- this._cancel();
+ this.cancel();
this._baseRanges = [];
this._revisionRanges = [];
}
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
index 9c2c6ed..4915eda 100644
--- a/polygerrit-ui/app/elements/gr-app-element.js
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -30,6 +30,7 @@
import './edit/gr-editor-view/gr-editor-view.js';
import './plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
import './plugins/gr-endpoint-param/gr-endpoint-param.js';
+import './plugins/gr-endpoint-slot/gr-endpoint-slot.js';
import './plugins/gr-external-style/gr-external-style.js';
import './plugins/gr-plugin-host/gr-plugin-host.js';
import './settings/gr-cla-view/gr-cla-view.js';
@@ -349,6 +350,8 @@
this.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
this.bindShortcut(
this.Shortcut.TOGGLE_BLAME, 'b');
+ this.bindShortcut(
+ this.Shortcut.TOGGLE_HIDE_ALL_COMMENT_THREADS, 'h');
this.bindShortcut(
this.Shortcut.OPEN_FIRST_FILE, ']');
@@ -496,6 +499,10 @@
}
}
+ handleShowKeyboardShortcuts() {
+ this.$.keyboardShortcuts.open();
+ }
+
_showKeyboardShortcuts(e) {
// same shortcut should close the dialog if pressed again
// when dialog is open
diff --git a/polygerrit-ui/app/elements/gr-app-element_html.js b/polygerrit-ui/app/elements/gr-app-element_html.js
index 47139ce..75295bc 100644
--- a/polygerrit-ui/app/elements/gr-app-element_html.js
+++ b/polygerrit-ui/app/elements/gr-app-element_html.js
@@ -104,6 +104,7 @@
id="mainHeader"
search-query="{{params.query}}"
on-mobile-search="_mobileSearchToggle"
+ on-show-keyboard-shortcuts="handleShowKeyboardShortcuts"
login-url="[[_loginUrl]]"
>
</gr-main-header>
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
index 52c4f4e..3f8aa44 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
@@ -14,13 +14,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-attribute-helper">
-
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
/** @constructor */
export function GrAttributeHelper(element) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
index ea60dc6..89d8ec2 100644
--- a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
@@ -14,13 +14,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-change-metadata-api">
-
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
/** @constructor */
export function GrChangeMetadataApi(plugin) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
index 41d5fd5..4822163 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
@@ -14,14 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-dom-hooks">
-
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
/** @constructor */
export function GrDomHooksManager(plugin) {
@@ -61,13 +54,18 @@
}
GrDomHook.prototype._createPlaceholder = function(hookName) {
- Polymer({
- is: hookName,
- properties: {
- plugin: Object,
- content: Object,
- },
- });
+ class HookPlaceholder extends PolymerElement {
+ static get is() { return hookName; }
+
+ static get properties() {
+ return {
+ plugin: Object,
+ content: Object,
+ };
+ }
+ }
+
+ customElements.define(HookPlaceholder.is, HookPlaceholder);
};
GrDomHook.prototype.handleInstanceDetached = function(instance) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
index 9bfb7da..4991770 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
@@ -74,12 +74,22 @@
});
}
- _initDecoration(name, plugin) {
+ _initDecoration(name, plugin, slot) {
const el = document.createElement(name);
return this._initProperties(el, plugin,
this.getContentChildren().find(
el => el.nodeName !== 'GR-ENDPOINT-PARAM'))
- .then(el => this._appendChild(el));
+ .then(el => {
+ const slotEl = slot ?
+ dom(this).querySelector(`gr-endpoint-slot[name=${slot}]`) :
+ null;
+ if (slot && slotEl) {
+ slotEl.parentNode.insertBefore(el, slotEl.nextSibling);
+ } else {
+ this._appendChild(el);
+ }
+ return el;
+ });
}
_initReplacement(name, plugin) {
@@ -133,7 +143,7 @@
return dom(this.root).appendChild(el);
}
- _initModule({moduleName, plugin, type, domHook}) {
+ _initModule({moduleName, plugin, type, domHook, slot}) {
const name = plugin.getPluginName() + '.' + moduleName;
if (this._initializedPlugins.get(name)) {
return;
@@ -141,7 +151,7 @@
let initPromise;
switch (type) {
case 'decorate':
- initPromise = this._initDecoration(moduleName, plugin);
+ initPromise = this._initDecoration(moduleName, plugin, slot);
break;
case 'replace':
initPromise = this._initReplacement(moduleName, plugin);
@@ -162,16 +172,18 @@
super.ready();
this._endpointCallBack = this._initModule.bind(this);
pluginEndpoints.onNewEndpoint(this.name, this._endpointCallBack);
- pluginLoader.awaitPluginsLoaded()
- .then(() => Promise.all(
- pluginEndpoints.getPlugins(this.name).map(
- pluginUrl => this._import(pluginUrl)))
- )
- .then(() =>
- pluginEndpoints
- .getDetails(this.name)
- .forEach(this._initModule, this)
- );
+ if (this.name) {
+ pluginLoader.awaitPluginsLoaded()
+ .then(() => Promise.all(
+ pluginEndpoints.getPlugins(this.name).map(
+ pluginUrl => this._import(pluginUrl)))
+ )
+ .then(() =>
+ pluginEndpoints
+ .getDetails(this.name)
+ .forEach(this._initModule, this)
+ );
+ }
}
}
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
index d7eafa5..890a457 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
@@ -29,6 +29,10 @@
<div>
<gr-endpoint-decorator name="first">
<gr-endpoint-param name="someparam" value="barbar"></gr-endpoint-param>
+ <p>
+ <span>test slot</span>
+ <gr-endpoint-slot name="test"></gr-endpoint-slot>
+ </p>
</gr-endpoint-decorator>
<gr-endpoint-decorator name="second">
<gr-endpoint-param name="someparam" value="foofoo"></gr-endpoint-param>
@@ -44,6 +48,7 @@
import '../../../test/common-test-setup.js';
import './gr-endpoint-decorator.js';
import '../gr-endpoint-param/gr-endpoint-param.js';
+import '../gr-endpoint-slot/gr-endpoint-slot.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {resetPlugins} from '../../../test/test-utils.js';
import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
@@ -56,6 +61,7 @@
let sandbox;
let plugin;
let decorationHook;
+ let decorationHookWithSlot;
let replacementHook;
setup(done => {
@@ -69,6 +75,11 @@
'http://some/plugin/url.html');
// Decoration
decorationHook = plugin.registerCustomComponent('first', 'some-module');
+ decorationHookWithSlot = plugin.registerCustomComponent(
+ 'first',
+ 'some-module-2',
+ {slot: 'test'}
+ );
// Replacement
replacementHook = plugin.registerCustomComponent(
'second', 'other-module', {replace: true});
@@ -100,15 +111,34 @@
const [module] = modules;
assert.isOk(module);
assert.equal(module['someparam'], 'barbar');
- return decorationHook.getLastAttached().then(element => {
- assert.strictEqual(element, module);
- })
+ return decorationHook.getLastAttached()
+ .then(element => {
+ assert.strictEqual(element, module);
+ })
.then(() => {
element.remove();
assert.equal(decorationHook.getAllAttached().length, 0);
});
});
+ test('decoration with slot', () => {
+ const element =
+ container.querySelector('gr-endpoint-decorator[name="first"]');
+ const modules = [...dom(element).querySelectorAll('p > some-module-2')];
+ assert.equal(modules.length, 1);
+ const [module] = modules;
+ assert.isOk(module);
+ assert.equal(module['someparam'], 'barbar');
+ return decorationHookWithSlot.getLastAttached()
+ .then(element => {
+ assert.strictEqual(element, module);
+ })
+ .then(() => {
+ element.remove();
+ assert.equal(decorationHookWithSlot.getAllAttached().length, 0);
+ });
+ });
+
test('replacement', () => {
const element =
container.querySelector('gr-endpoint-decorator[name="second"]');
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-slot/gr-endpoint-slot.js
similarity index 62%
copy from polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js
copy to polygerrit-ui/app/elements/plugins/gr-endpoint-slot/gr-endpoint-slot.js
index cf934b0..9ee9c3d 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-slot/gr-endpoint-slot.js
@@ -14,21 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- display: block;
- margin-bottom: var(--spacing-xxl);
- }
- </style>
- <h3>[[title]]</h3>
- <gr-button
- title$="[[tooltip]]"
- disabled$="[[disabled]]"
- on-click="_onCommandTap"
- >
- [[title]]
- </gr-button>
-`;
+class GrEndpointSlot extends PolymerElement {
+ static get is() { return 'gr-endpoint-slot'; }
+
+ static get properties() {
+ return {
+ name: String,
+ };
+ }
+}
+
+customElements.define(GrEndpointSlot.is, GrEndpointSlot);
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
index da85b9e..466f84a 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
@@ -14,13 +14,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-event-helper">
-
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
/** @constructor */
export function GrEventHelper(element) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
index 738b276..e2dd047 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
@@ -16,13 +16,6 @@
*/
import './gr-plugin-popup.js';
import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-popup-interface">
-
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
/**
* Plugin popup API.
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js
index 752570f..1a2cd28 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js
@@ -14,20 +14,31 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../admin/gr-repo-command/gr-repo-command.js';
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-Polymer({
- _template: html`
- <gr-repo-command title="[[title]]">
- </gr-repo-command>
-`,
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-plugin-repo-command_html.js';
- is: 'gr-plugin-repo-command',
+class GrPluginRepoCommand extends PolymerElement {
+ static get is() {
+ return 'gr-plugin-repo-command';
+ }
- properties: {
- title: String,
- repoName: String,
- config: Object,
- },
-});
+ static get properties() {
+ return {
+ title: String,
+ repoName: String,
+ config: Object,
+ };
+ }
+
+ static get template() {
+ return htmlTemplate;
+ }
+
+ _handleClick() {
+ this.dispatchEvent(
+ new CustomEvent('command-tap', {composed: true, bubbles: true})
+ );
+ }
+}
+
+customElements.define(GrPluginRepoCommand.is, GrPluginRepoCommand);
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command_html.js
similarity index 86%
rename from polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js
rename to polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command_html.js
index cf934b0..1e5088b3 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command_html.js
@@ -24,11 +24,5 @@
}
</style>
<h3>[[title]]</h3>
- <gr-button
- title$="[[tooltip]]"
- disabled$="[[disabled]]"
- on-click="_onCommandTap"
- >
- [[title]]
- </gr-button>
+ <gr-button on-click="_handleClick">[[title]]</gr-button>
`;
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js
index 445356d..04408f8 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js
@@ -15,13 +15,6 @@
* limitations under the License.
*/
import './gr-plugin-repo-command.js';
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-repo-api">
-
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
/** @constructor */
export function GrRepoApi(plugin) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
index 32ae959..1af91fd 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
@@ -73,13 +73,12 @@
const pluginCommand = element.shadowRoot
.querySelector('gr-plugin-repo-command');
assert.isOk(pluginCommand);
- const command = pluginCommand.shadowRoot
- .querySelector('gr-repo-command');
- assert.isOk(command);
- assert.equal(command.title, 'foo');
+ const btn = pluginCommand.shadowRoot
+ .querySelector('gr-button');
+ assert.isOk(btn);
+ assert.equal(btn.textContent, 'foo');
assert.isFalse(tapStub.called);
- MockInteractions.tap(command.shadowRoot
- .querySelector('gr-button'));
+ MockInteractions.tap(btn);
assert.isTrue(tapStub.called);
done();
});
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
index 35396cf..8050cd6 100644
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
@@ -16,13 +16,6 @@
*/
import '../../settings/gr-settings-view/gr-settings-item.js';
import '../../settings/gr-settings-view/gr-settings-menu-item.js';
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-settings-api">
-
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
/** @constructor */
export function GrSettingsApi(plugin) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js
index ae8b8ab..1e37603 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js
@@ -14,10 +14,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-Polymer({
- _template: html`
+
+class CustomPluginHeader extends PolymerElement {
+ static get is() {
+ return 'gr-custom-plugin-header';
+ }
+
+ static get properties() {
+ return {
+ logoUrl: String,
+ title: String,
+ };
+ }
+
+ static get template() {
+ return html`
<style>
img {
width: 1em;
@@ -32,12 +45,8 @@
<img src="[[logoUrl]]" hidden\$="[[!logoUrl]]">
<span class="title">[[title]]</span>
</span>
-`,
+`;
+ }
+}
- is: 'gr-custom-plugin-header',
-
- properties: {
- logoUrl: String,
- title: String,
- },
-});
+customElements.define(CustomPluginHeader.is, CustomPluginHeader);
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
index be427ec..48b14d3 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
@@ -15,13 +15,6 @@
* limitations under the License.
*/
import './gr-custom-plugin-header.js';
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-theme-api">
-
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
/** @constructor */
export function GrThemeApi(plugin) {
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js
index 2d371e2..b1a951b7 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js
@@ -61,8 +61,8 @@
/* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
</style>
<main>
- <h1>New Contributor Agreement</h1>
- <h3>Select an agreement type:</h3>
+ <h1 class="heading-1">New Contributor Agreement</h1>
+ <h3 class="heading-3">Select an agreement type:</h3>
<template
is="dom-repeat"
items="[[_serverConfig.auth.contributor_agreements]]"
@@ -92,7 +92,7 @@
id="claNewAgreement"
class$="[[_computeShowAgreementsClass(_showAgreements)]]"
>
- <h3 class="smallHeading">Review the agreement:</h3>
+ <h3 class="heading-3">Review the agreement:</h3>
<div id="agreementsUrl" class="agreementsUrl">
<a href$="[[_agreementsUrl]]" target="blank" rel="noopener">
Please review the agreement.</a
@@ -101,7 +101,7 @@
<div
class$="agreementsTextBox [[_computeHideAgreementClass(_agreementName, _serverConfig.auth.contributor_agreements)]]"
>
- <h3 class="smallHeading">Complete the agreement:</h3>
+ <h3 class="heading-3">Complete the agreement:</h3>
<iron-input
bind-value="{{_agreementsText}}"
placeholder="Enter 'I agree' here"
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js
index e26faab..26200d4 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js
@@ -23,6 +23,6 @@
margin-bottom: var(--spacing-xxl);
}
</style>
- <h2 id="[[anchor]]">[[title]]</h2>
+ <h2 id="[[anchor]]" class="heading-2">[[title]]</h2>
<slot></slot>
`;
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js
index b22d412..e32a551 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js
@@ -21,6 +21,12 @@
:host {
color: var(--primary-text-color);
}
+ h2 {
+ font-family: var(--header-font-family);
+ font-size: var(--font-size-h2);
+ font-weight: var(--font-weight-h2);
+ line-height: var(--line-height-h2);
+ }
.newEmailInput {
width: 20em;
}
@@ -94,7 +100,7 @@
</ul>
</gr-page-nav>
<main class="gr-form-styles">
- <h1>User Settings</h1>
+ <h1 class="heading-1">User Settings</h1>
<section class="darkToggle">
<div class="toggle">
<paper-toggle-button
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
index abe5206..6f5f9e4 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
@@ -158,10 +158,10 @@
}
_handleAdd(e) {
- this._addAccountItem(e.detail.value);
+ this.addAccountItem(e.detail.value);
}
- _addAccountItem(item) {
+ addAccountItem(item) {
// Append new account or group to the accounts property. We add our own
// internal properties to the account/group here, so we clone the object
// to avoid cluttering up the shared change object.
@@ -248,11 +248,11 @@
_handleRemove(e) {
const toRemove = e.detail.account;
- this._removeAccount(toRemove);
+ this.removeAccount(toRemove);
this.$.entry.focus();
}
- _removeAccount(toRemove) {
+ removeAccount(toRemove) {
if (!toRemove || !this._computeRemovable(toRemove, this.readonly)) {
return;
}
@@ -286,7 +286,7 @@
}
switch (e.detail.keyCode) {
case 8: // Backspace
- this._removeAccount(this.accounts[this.accounts.length - 1]);
+ this.removeAccount(this.accounts[this.accounts.length - 1]);
break;
case 37: // Left arrow
if (this.accountChips[this.accountChips.length - 1]) {
@@ -305,7 +305,7 @@
case 13: // Enter
case 32: // Spacebar
case 46: // Delete
- this._removeAccount(chip.account);
+ this.removeAccount(chip.account);
// Splice from this array to avoid inconsistent ordering of
// event handling.
chips.splice(index, 1);
@@ -345,7 +345,7 @@
submitEntryText() {
const text = this.$.entry.getText();
if (!text.length) { return true; }
- const wasSubmitted = this._addAccountItem(text);
+ const wasSubmitted = this.addAccountItem(text);
if (wasSubmitted) { this.$.entry.clear(); }
return wasSubmitted;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
index 2efc6e8..41158a8 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
@@ -351,7 +351,7 @@
element.readonly = true;
const acct = makeAccount();
element.accounts = [acct];
- element._removeAccount(acct);
+ element.removeAccount(acct);
assert.equal(element.accounts.length, 1);
});
@@ -537,7 +537,7 @@
element.accounts = [makeAccount(), makeAccount()];
flush(() => {
const focusSpy = sandbox.spy(element.accountChips[1], 'focus');
- const removeSpy = sandbox.spy(element, '_removeAccount');
+ const removeSpy = sandbox.spy(element, 'removeAccount');
MockInteractions.pressAndReleaseKeyOn(
element.accountChips[0], 8); // Backspace
assert.isTrue(focusSpy.called);
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.js
similarity index 93%
rename from polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
rename to polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.js
index 6dd5a97..eb05b90 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.js
@@ -1,40 +1,28 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @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.
+ */
-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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-reviewer-list</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
- <template>
- <gr-autocomplete no-debounce></gr-autocomplete>
- </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
import './gr-autocomplete.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
import {dom, flush as flush$0} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
+const basicFixture = fixtureFromTemplate(
+ html`<gr-autocomplete no-debounce></gr-autocomplete>`);
+
suite('gr-autocomplete tests', () => {
let element;
let sandbox;
@@ -44,7 +32,7 @@
};
setup(() => {
- element = fixture('basic');
+ element = basicFixture.instantiate();
sandbox = sinon.sandbox.create();
});
@@ -609,4 +597,3 @@
});
});
});
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
index bdac50f..346d84e 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -23,7 +23,7 @@
import {htmlTemplate} from './gr-button_html.js';
import {TooltipBehavior} from '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
-import {util} from '../../../scripts/util.js';
+import {getEventPath} from '../../../utils/dom-util.js';
import {appContext} from '../../../services/app-context.js';
/**
@@ -113,7 +113,7 @@
}
this.reporting.reportInteraction('button-click',
- {path: util.getEventPath(e)});
+ {path: getEventPath(e)});
}
_disabledChanged(disabled) {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
index 0e2c8a3..fa686da 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
@@ -155,6 +155,14 @@
value: false,
reflectToAttribute: true,
},
+ showFileName: {
+ type: Boolean,
+ value: true,
+ },
+ showPatchset: {
+ type: Boolean,
+ value: true,
+ },
};
}
@@ -221,6 +229,11 @@
{detail: {rootId: this.rootId}, bubbles: false}));
}
+ _getDiffUrlForPath(path) {
+ return GerritNav.getUrlForDiffById(this.changeNum, this.projectName, path,
+ this.patchNum);
+ }
+
_getDiffUrlForComment(projectName, changeNum, path, patchNum) {
return GerritNav.getUrlForDiffById(changeNum,
projectName, path, patchNum,
@@ -232,8 +245,24 @@
}
_computeDisplayPath(path) {
- const lineString = this.lineNum ? `#${this.lineNum}` : '';
- return this.computeDisplayPath(path) + lineString;
+ const displayPath = this.computeDisplayPath(path);
+ if (displayPath === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
+ return `Patchset ${this.patchNum}`;
+ }
+ return displayPath;
+ }
+
+ _computeDisplayLine() {
+ if (this.lineNum) return `#${this.lineNum}`;
+ // If range is set, then lineNum equals the end line of the range.
+ if (!this.lineNum && !this.range) {
+ if (this.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) {
+ return '';
+ }
+ return 'FILE';
+ }
+ if (this.range) return `#${this.range.end_line}`;
+ return '';
}
_getLoggedIn() {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
index 88f0ff6..837a58f 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
@@ -37,7 +37,7 @@
#container {
background-color: var(--comment-background-color);
color: var(--comment-text-color);
- display: block;
+ display: var(--gr-comment-thread-display, block);
margin: 0 var(--spacing-s) var(--spacing-s);
white-space: normal;
box-shadow: var(--elevation-level-2);
@@ -72,19 +72,29 @@
margin-left: var(--spacing-m);
font-style: italic;
}
+ .fileName {
+ padding: var(--spacing-m) var(--spacing-s) var(--spacing-m);
+ }
</style>
<template is="dom-if" if="[[showFilePath]]">
+ <template is="dom-if" if="[[showFileName]]">
+ <div class="fileName">
+ <template is="dom-if" if="[[_isPatchsetLevelComment(path)]]">
+ <span> [[_computeDisplayPath(path)]] </span>
+ </template>
+ <template is="dom-if" if="[[!_isPatchsetLevelComment(path)]]">
+ <a href$="[[_getDiffUrlForPath(path)]]">
+ [[_computeDisplayPath(path)]]
+ </a>
+ </template>
+ </div>
+ </template>
<div class="pathInfo">
- <template is="dom-if" if="[[_isPatchsetLevelComment(path)]]">
- <span>Patchset Comment</span>
- <span class="descriptionText">Patchset [[patchNum]]</span>
- </template>
<template is="dom-if" if="[[!_isPatchsetLevelComment(path)]]">
<a
href$="[[_getDiffUrlForComment(projectName, changeNum, path, patchNum)]]"
- >[[_computeDisplayPath(path)]]</a
+ >[[_computeDisplayLine()]]</a
>
- <span class="descriptionText">Patchset [[patchNum]]</span>
</template>
</div>
</template>
@@ -106,6 +116,7 @@
patch-num="[[patchNum]]"
draft="[[_isDraft(comment)]]"
show-actions="[[_showActions]]"
+ show-patchset="[[showPatchset]]"
comment-side="[[comment.__commentSide]]"
side="[[comment.side]]"
project-config="[[_projectConfig]]"
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
index eaa6547..e219b22 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
@@ -42,6 +42,7 @@
import './gr-comment-thread.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {SpecialFilePath} from '../../../constants/constants.js';
suite('gr-comment-thread tests', () => {
suite('basic test', () => {
@@ -215,17 +216,37 @@
assert.notEqual(getComputedStyle(element.shadowRoot
.querySelector('.pathInfo')).display,
'none');
- assert.isTrue(GerritNav.getUrlForDiffById.lastCall.calledWithExactly(
+ assert.isTrue(GerritNav.getUrlForDiffById.getCall(0).calledWithExactly(
element.changeNum, element.projectName, element.path,
element.patchNum, null, element.lineNum));
+ assert.isTrue(GerritNav.getUrlForDiffById.getCall(1).calledWithExactly(
+ element.changeNum, element.projectName, element.path,
+ element.patchNum));
});
test('_computeDisplayPath', () => {
- const path = 'path/to/file';
+ let path = 'path/to/file';
assert.equal(element._computeDisplayPath(path), 'path/to/file');
element.lineNum = 5;
- assert.equal(element._computeDisplayPath(path), 'path/to/file#5');
+ assert.equal(element._computeDisplayPath(path), 'path/to/file');
+
+ element.patchNum = '3';
+ path = SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
+ assert.equal(element._computeDisplayPath(path), 'Patchset 3');
+ });
+
+ test('_computeDisplayLine', () => {
+ element.lineNum = 5;
+ assert.equal(element._computeDisplayLine(), '#5');
+
+ element.path = SpecialFilePath.COMMIT_MESSAGE;
+ element.lineNum = 5;
+ assert.equal(element._computeDisplayLine(), '#5');
+
+ element.lineNum = undefined;
+ element.path = SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
+ assert.equal(element._computeDisplayLine(), '');
});
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
index 6697880..02fec5a 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
@@ -213,6 +213,10 @@
type: Boolean,
value: false,
},
+ showPatchset: {
+ type: Boolean,
+ value: true,
+ },
_respectfulReviewTip: String,
_respectfulTipDismissed: {
type: Boolean,
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
index 8b3ef8a..1d04969 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
@@ -60,7 +60,6 @@
}
.date {
justify-content: flex-end;
- margin-left: var(--spacing-m);
text-align: right;
white-space: nowrap;
}
@@ -232,6 +231,10 @@
.pointer {
cursor: pointer;
}
+ .patchset-text {
+ color: var(--deemphasized-text-color);
+ margin-left: var(--spacing-s);
+ }
</style>
<div id="container" class="container">
<div class="header" id="header" on-click="_handleToggleCollapsed">
@@ -270,6 +273,10 @@
>
<iron-icon id="icon" icon="gr-icons:delete"></iron-icon>
</gr-button>
+ <template is="dom-if" if="[[showPatchset]]">
+ <span class="patchset-text"> Patchset [[patchNum]]</span>
+ </template>
+ <span class="separator"></span>
<span class="date" tabindex="0" on-click="_handleAnchorClick">
<gr-date-formatter
has-tooltip=""
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js
index b32f871..54b404d 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js
@@ -60,7 +60,7 @@
}
</style>
<div class="container" on-keydown="_handleKeydown">
- <header class="font-h3"><slot name="header"></slot></header>
+ <header class="heading-3"><slot name="header"></slot></header>
<main>
<div class="overflow-container">
<slot name="main"></slot>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index 8e7ea87..717275d 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -292,7 +292,8 @@
const item = this.items.find(item => item.id === id);
if (id && !this.disabledIds.includes(id)) {
if (item) {
- this.dispatchEvent(new CustomEvent('tap-item', {detail: item}));
+ this.dispatchEvent(new CustomEvent('tap-item',
+ {detail: item, bubbles: true, composed: true}));
}
this.dispatchEvent(new CustomEvent('tap-item-' + id));
}
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js
index 9c8788b..bbf6a96 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js
@@ -70,7 +70,7 @@
<gr-avatar account="[[account]]" image-size="56"></gr-avatar>
</div>
<div class="account">
- <h3 class="name">[[account.name]]</h3>
+ <h3 class="name heading-3">[[account.name]]</h3>
<div class="email">[[account.email]]</div>
</div>
</div>
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
index 254fd25..070ac02 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
@@ -102,8 +102,8 @@
<g id="pets"><circle cx="4.5" cy="9.5" r="2.5"/><circle cx="9" cy="5.5" r="2.5"/><circle cx="15" cy="5.5" r="2.5"/><circle cx="19.5" cy="9.5" r="2.5"/><path d="M17.34 14.86c-.87-1.02-1.6-1.89-2.48-2.91-.46-.54-1.05-1.08-1.75-1.32-.11-.04-.22-.07-.33-.09-.25-.04-.52-.04-.78-.04s-.53 0-.79.05c-.11.02-.22.05-.33.09-.7.24-1.28.78-1.75 1.32-.87 1.02-1.6 1.89-2.48 2.91-1.31 1.31-2.92 2.76-2.62 4.79.29 1.02 1.02 2.03 2.33 2.32.73.15 3.06-.44 5.54-.44h.18c2.48 0 4.81.58 5.54.44 1.31-.29 2.04-1.31 2.33-2.32.31-2.04-1.3-3.49-2.61-4.8z"/><path d="M0 0h24v24H0z" fill="none"/></g>
<!-- This SVG is a copy from material.io https://material.io/icons/#visibility-->
<g id="ready"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></g>
- <!-- This SVG is a copy from material.io https://material.io/resources/icons/?icon=link&style=baseline-->
- <g id="link"><path d="M0 0h24v24H0z" fill="none"/><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></g>
+ <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons -->
+ <g id="schedule"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"></path></g>
</defs>
</svg>
</iron-iconset-svg>`;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js
index 388243c..0727397 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js
@@ -39,12 +39,13 @@
}
};
-GrPluginEndpoints.prototype._getOrCreateModuleInfo = function(plugin,
- endpoint, type, moduleName, domHook) {
+GrPluginEndpoints.prototype._getOrCreateModuleInfo = function(plugin, opts) {
+ const {endpoint, slot, type, moduleName, domHook} = opts;
const existingModule = this._endpoints[endpoint].find(info =>
info.plugin === plugin &&
info.moduleName === moduleName &&
- info.domHook === domHook
+ info.domHook === domHook &&
+ info.slot === slot
);
if (existingModule) {
return existingModule;
@@ -55,6 +56,7 @@
pluginUrl: plugin._url,
type,
domHook,
+ slot,
};
this._endpoints[endpoint].push(newModule);
return newModule;
@@ -67,9 +69,12 @@
* Dynamic plugins are registered to a specific prefix, such as
* 'change-list-header'. These plugins are then fetched by prefix to determine
* which endpoints to dynamically add to the page.
+ *
+ * @param {Object} plugin
+ * @param {Object} opts
*/
-GrPluginEndpoints.prototype.registerModule = function(plugin, endpoint, type,
- moduleName, domHook, dynamicEndpoint) {
+GrPluginEndpoints.prototype.registerModule = function(plugin, opts) {
+ const {endpoint, dynamicEndpoint} = opts;
if (dynamicEndpoint) {
if (!this._dynamicPlugins[dynamicEndpoint]) {
this._dynamicPlugins[dynamicEndpoint] = new Set();
@@ -79,8 +84,7 @@
if (!this._endpoints[endpoint]) {
this._endpoints[endpoint] = [];
}
- const moduleInfo = this._getOrCreateModuleInfo(plugin, endpoint, type,
- moduleName, domHook);
+ const moduleInfo = this._getOrCreateModuleInfo(plugin, opts);
if (pluginLoader.arePluginsLoaded() && this._callbacks[endpoint]) {
this._callbacks[endpoint].forEach(callback => callback(moduleInfo));
}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
index ec8710b..3494e99 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
@@ -48,11 +48,25 @@
pluginApi.install(p => { pluginFoo = p; }, '0.1',
'http://test.com/plugins/testplugin/static/foo.html');
instance.registerModule(
- pluginFoo, 'a-place', 'decorate', 'foo-module', domHook);
+ pluginFoo,
+ {
+ endpoint: 'a-place',
+ type: 'decorate',
+ moduleName: 'foo-module',
+ domHook,
+ }
+ );
pluginApi.install(p => { pluginBar = p; }, '0.1',
'http://test.com/plugins/testplugin/static/bar.html');
instance.registerModule(
- pluginBar, 'a-place', 'style', 'bar-module', domHook);
+ pluginBar,
+ {
+ endpoint: 'a-place',
+ type: 'style',
+ moduleName: 'bar-module',
+ domHook,
+ }
+ );
sandbox.stub(pluginLoader, 'arePluginsLoaded').returns(true);
});
@@ -68,6 +82,7 @@
pluginUrl: pluginFoo._url,
type: 'decorate',
domHook,
+ slot: undefined,
},
{
moduleName: 'bar-module',
@@ -75,6 +90,7 @@
pluginUrl: pluginBar._url,
type: 'style',
domHook,
+ slot: undefined,
},
]);
});
@@ -87,6 +103,7 @@
pluginUrl: pluginBar._url,
type: 'style',
domHook,
+ slot: undefined,
},
]);
});
@@ -101,6 +118,7 @@
pluginUrl: pluginFoo._url,
type: 'decorate',
domHook,
+ slot: undefined,
},
]);
});
@@ -119,13 +137,20 @@
const newModuleStub = sandbox.stub();
instance.onNewEndpoint('a-place', newModuleStub);
instance.registerModule(
- pluginFoo, 'a-place', 'replace', 'zaz-module', domHook);
+ pluginFoo,
+ {
+ endpoint: 'a-place',
+ type: 'replace',
+ moduleName: 'zaz-module',
+ domHook,
+ });
assert.deepEqual(newModuleStub.lastCall.args[0], {
moduleName: 'zaz-module',
plugin: pluginFoo,
pluginUrl: pluginFoo._url,
type: 'replace',
domHook,
+ slot: undefined,
});
});
@@ -139,6 +164,7 @@
pluginUrl: pluginFoo._url,
type: 'decorate',
domHook,
+ slot: undefined,
},
{
moduleName: 'bar-module',
@@ -146,6 +172,7 @@
pluginUrl: pluginBar._url,
type: 'style',
domHook,
+ slot: undefined,
},
]);
});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index d111d5c..9d79462 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -94,9 +94,9 @@
return this._name;
};
- Plugin.prototype.registerStyleModule = function(endpointName, moduleName) {
+ Plugin.prototype.registerStyleModule = function(endpoint, moduleName) {
pluginEndpoints.registerModule(
- this, endpointName, EndpointType.STYLE, moduleName);
+ this, {endpoint, type: EndpointType.STYLE, moduleName});
};
/**
@@ -122,14 +122,15 @@
};
Plugin.prototype._registerCustomComponent = function(
- endpointName, opt_moduleName, opt_options, dynamicEndpoint) {
+ endpoint, opt_moduleName, opt_options, dynamicEndpoint) {
const type = opt_options && opt_options.replace ?
EndpointType.REPLACE : EndpointType.DECORATE;
- const hook = this._domHooks.getDomHook(endpointName, opt_moduleName);
- const moduleName = opt_moduleName || hook.getModuleName();
+ const slot = opt_options && opt_options.slot || '';
+ const domHook = this._domHooks.getDomHook(endpoint, opt_moduleName);
+ const moduleName = opt_moduleName || domHook.getModuleName();
pluginEndpoints.registerModule(
- this, endpointName, type, moduleName, hook, dynamicEndpoint);
- return hook.getPublicAPI();
+ this, {slot, endpoint, type, moduleName, domHook, dynamicEndpoint});
+ return domHook.getPublicAPI();
};
/**
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
index 7402e22..a4adbac 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
@@ -14,13 +14,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-etag-decorator">
-
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
// Limit cache size because /change/detail responses may be large.
const MAX_CACHE_SIZE = 30;
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 2d951d4..de4f12d 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -14,15 +14,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-/* NB: es6-promise Needed for IE11 and fetch polyfill support, see Issue 4308 */
/* NB: Order is important, because of namespaced classes. */
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import 'es6-promise/lib/es6-promise.js';
-import 'whatwg-fetch/fetch.js';
import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
import {PathListBehavior} from '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
index c89378c..9b4bbaf 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
@@ -17,14 +17,7 @@
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-const $_documentContainer = document.createElement('template');
-
-$_documentContainer.innerHTML = `<dom-module id="gr-select">
- <slot></slot>
-
-</dom-module>`;
-
-document.head.appendChild($_documentContainer.content);
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
/**
* @extends PolymerElement
@@ -33,6 +26,12 @@
LegacyElementMixin(PolymerElement)) {
static get is() { return 'gr-select'; }
+ static get template() {
+ return html`
+ <slot></slot>
+ `;
+ }
+
static get properties() {
return {
bindValue: {
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init.js b/polygerrit-ui/app/embed/gr-diff-app-context-init.js
index 8a7fb66..90110f0 100644
--- a/polygerrit-ui/app/embed/gr-diff-app-context-init.js
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init.js
@@ -24,7 +24,7 @@
}
/**
- * @returns {string[]} array of all enabled experiments.
+ * @returns {!Array<string>} array of all enabled experiments.
*/
get enabledExperiments() {
return [];
diff --git a/polygerrit-ui/app/node_modules_licenses/licenses.ts b/polygerrit-ui/app/node_modules_licenses/licenses.ts
index 4c259cd..1141d6b 100644
--- a/polygerrit-ui/app/node_modules_licenses/licenses.ts
+++ b/polygerrit-ui/app/node_modules_licenses/licenses.ts
@@ -261,14 +261,6 @@
}
},
{
- name: "es6-promise",
- license: {
- name: "es6-promise",
- type: LicenseTypes.Mit,
- packageLicenseFile: "LICENSE"
- }
- },
- {
name: "isarray",
license: SharedLicenses.IsArray
},
@@ -291,14 +283,6 @@
{
name: "polymer-bridges",
license: SharedLicenses.Polymer2018
- },
- {
- name: "whatwg-fetch",
- license: {
- name: "whatwg-fetch",
- type: LicenseTypes.Mit,
- packageLicenseFile: "LICENSE"
- }
}
];
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index a9668c3..32560ff 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -25,12 +25,10 @@
"@polymer/polymer": "^3.3.0",
"@webcomponents/shadycss": "^1.9.2",
"@webcomponents/webcomponentsjs": "^1.3.3",
- "es6-promise": "^3.3.1",
"page": "^1.11.5",
"polymer-bridges": "file:../../polymer-bridges/",
"ba-linkify": "file:../../lib/ba-linkify/src/",
- "polymer-resin": "^2.0.1",
- "whatwg-fetch": "^3.0.0"
+ "polymer-resin": "^2.0.1"
},
"license": "Apache-2.0",
"private": true
diff --git a/polygerrit-ui/app/samples/add-from-favorite-reviewers.js b/polygerrit-ui/app/samples/add-from-favorite-reviewers.js
new file mode 100644
index 0000000..76b2787
--- /dev/null
+++ b/polygerrit-ui/app/samples/add-from-favorite-reviewers.js
@@ -0,0 +1,145 @@
+/**
+ * @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.
+ */
+/**
+ * This plugin will a button to quickly add favorite reviewers to
+ * reviewers in reply dialog.
+ */
+
+const onToggleButtonClicks = [];
+function toggleButtonClicked(expanded) {
+ onToggleButtonClicks.forEach(cb => {
+ cb(expanded);
+ });
+}
+
+class ReviewerShortcut extends Polymer.Element {
+ static get is() { return 'reviewer-shortcut'; }
+
+ static get properties() {
+ return {
+ change: Object,
+ expanded: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
+
+ static get template() {
+ return Polymer.html`
+ <button on-click="toggleControlContent">
+ [[computeButtonText(expanded)]]
+ </button>
+ `;
+ }
+
+ toggleControlContent() {
+ this.expanded = !this.expanded;
+ toggleButtonClicked(this.expanded);
+ }
+
+ computeButtonText(expanded) {
+ return expanded ? 'Collapse' : 'Add favorite reviewers';
+ }
+}
+
+customElements.define(ReviewerShortcut.is, ReviewerShortcut);
+
+class ReviewerShortcutContent extends Polymer.Element {
+ static get is() { return 'reviewer-shortcut-content'; }
+
+ static get properties() {
+ return {
+ change: Object,
+ hidden: {
+ type: Boolean,
+ value: true,
+ reflectToAttribute: true,
+ },
+ };
+ }
+
+ static get template() {
+ return Polymer.html`
+ <style>
+ :host([hidden]) {
+ display: none;
+ }
+ :host {
+ display: block;
+ }
+ </style>
+ <ul>
+ <li><button on-click="addApple">Apple</button></li>
+ <li><button on-click="addBanana">Banana</button></li>
+ <li><button on-click="addCherry">Cherry</button></li>
+ </ul>
+ `;
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ onToggleButtonClicks.push(expanded => {
+ this.hidden = !expanded;
+ });
+ }
+
+ addApple() {
+ this.dispatchEvent(new CustomEvent('add-reviewer', {detail: {
+ reviewer: {
+ display_name: 'Apple',
+ email: 'apple@gmail.com',
+ name: 'Apple',
+ _account_id: 0,
+ },
+ },
+ composed: true, bubbles: true}));
+ }
+
+ addBanana() {
+ this.dispatchEvent(new CustomEvent('add-reviewer', {detail: {
+ reviewer: {
+ display_name: 'Banana',
+ email: 'banana@gmail.com',
+ name: 'B',
+ _account_id: 1,
+ },
+ },
+ composed: true, bubbles: true}));
+ }
+
+ addCherry() {
+ this.dispatchEvent(new CustomEvent('add-reviewer', {detail: {
+ reviewer: {
+ display_name: 'Cherry',
+ email: 'cherry@gmail.com',
+ name: 'C',
+ _account_id: 2,
+ },
+ },
+ composed: true, bubbles: true}));
+ }
+}
+
+customElements.define(ReviewerShortcutContent.is, ReviewerShortcutContent);
+
+Gerrit.install(plugin => {
+ plugin.registerCustomComponent(
+ 'reply-reviewers', ReviewerShortcut.is, {slot: 'right'});
+ plugin.registerCustomComponent(
+ 'reply-reviewers', ReviewerShortcutContent.is, {slot: 'below'});
+});
\ No newline at end of file
diff --git a/polygerrit-ui/app/samples/repo-command.js b/polygerrit-ui/app/samples/repo-command.js
index 6b6ed98..5aaea30 100644
--- a/polygerrit-ui/app/samples/repo-command.js
+++ b/polygerrit-ui/app/samples/repo-command.js
@@ -33,11 +33,19 @@
static get template() {
return html`
- <gr-repo-command
- title="Low-level bork"
- on-command-tap="_handleCommandTap">
- </gr-repo-command>
- `;
+ <style include="shared-styles">
+ :host {
+ display: block;
+ margin-bottom: var(--spacing-xxl);
+ }
+ </style>
+ <h3>Low-level bork</h3>
+ <gr-button
+ on-click="_handleCommandTap"
+ >
+ Low-level bork
+ </gr-button>
+ `;
}
connectedCallback() {
diff --git a/polygerrit-ui/app/samples/theme-plugin.js b/polygerrit-ui/app/samples/theme-plugin.js
index f3a8931..b3d4033 100644
--- a/polygerrit-ui/app/samples/theme-plugin.js
+++ b/polygerrit-ui/app/samples/theme-plugin.js
@@ -15,7 +15,6 @@
* limitations under the License.
*/
const customTheme = document.createElement('dom-module');
-customTheme.id = 'theme-plugin';
customTheme.innerHTML = `
<template>
<style>
@@ -25,9 +24,9 @@
</style>
</template>
`;
+customTheme.register('theme-plugin');
const darkCustomTheme = document.createElement('dom-module');
-darkCustomTheme.id = 'dark-theme-plugin';
darkCustomTheme.innerHTML = `
<template>
<style>
@@ -37,6 +36,7 @@
</style>
</template>
`;
+darkCustomTheme.register('dark-theme-plugin');
/**
* This plugin will change the primary text color to red.
diff --git a/polygerrit-ui/app/scripts/util.js b/polygerrit-ui/app/scripts/util.js
index 0103bf2..e4be858 100644
--- a/polygerrit-ui/app/scripts/util.js
+++ b/polygerrit-ui/app/scripts/util.js
@@ -15,17 +15,6 @@
* limitations under the License.
*/
-function getPathFromNode(el) {
- if (!el.tagName || el.tagName === 'GR-APP'
- || el instanceof DocumentFragment
- || el instanceof HTMLSlotElement) {
- return '';
- }
- let path = el.tagName.toLowerCase();
- if (el.id) path += `#${el.id}`;
- if (el.className) path += `.${el.className.replace(/ /g, '.')}`;
- return path;
-}
// TODO (dmfilippov): Each function must be exported separately. According to
// the code style guide, a namespacing is not allowed.
export const util = {
@@ -76,133 +65,4 @@
};
return wrappedPromise;
},
-
- /**
- * Get computed style value.
- *
- * If ShadyCSS is provided, use ShadyCSS api.
- * If `getComputedStyleValue` is provided on the element, use it.
- * Otherwise fallback to native method (in polymer 2).
- *
- */
- getComputedStyleValue: (name, el) => {
- let style;
- if (window.ShadyCSS) {
- style = ShadyCSS.getComputedStyleValue(el, name);
- } else if (el.getComputedStyleValue) {
- style = el.getComputedStyleValue(name);
- } else {
- style = getComputedStyle(el).getPropertyValue(name);
- }
- return style;
- },
-
- /**
- * Query selector on a dom element.
- *
- * This is shadow DOM compatible, but only works when selector is within
- * one shadow host, won't work if your selector is crossing
- * multiple shadow hosts.
- *
- */
- querySelector: (el, selector) => {
- let nodes = [el];
- let result = null;
- while (nodes.length) {
- const node = nodes.pop();
-
- // Skip if it's an invalid node.
- if (!node || !node.querySelector) continue;
-
- // Try find it with native querySelector directly
- result = node.querySelector(selector);
-
- if (result) {
- break;
- }
-
- // Add all nodes with shadowRoot and loop through
- const allShadowNodes = [...node.querySelectorAll('*')]
- .filter(child => !!child.shadowRoot)
- .map(child => child.shadowRoot);
- nodes = nodes.concat(allShadowNodes);
-
- // Add shadowRoot of current node if has one
- // as its not included in node.querySelectorAll('*')
- if (node.shadowRoot) {
- nodes.push(node.shadowRoot);
- }
- }
- return result;
- },
-
- /**
- * Query selector all dom elements matching with certain selector.
- *
- * This is shadow DOM compatible, but only works when selector is within
- * one shadow host, won't work if your selector is crossing
- * multiple shadow hosts.
- *
- * Note: this can be very expensive, only use when have to.
- */
- querySelectorAll: (el, selector) => {
- let nodes = [el];
- const results = new Set();
- while (nodes.length) {
- const node = nodes.pop();
-
- if (!node || !node.querySelectorAll) continue;
-
- // Try find all from regular children
- [...node.querySelectorAll(selector)]
- .forEach(el => results.add(el));
-
- // Add all nodes with shadowRoot and loop through
- const allShadowNodes = [...node.querySelectorAll('*')]
- .filter(child => !!child.shadowRoot)
- .map(child => child.shadowRoot);
- nodes = nodes.concat(allShadowNodes);
-
- // Add shadowRoot of current node if has one
- // as its not included in node.querySelectorAll('*')
- if (node.shadowRoot) {
- nodes.push(node.shadowRoot);
- }
- }
- return [...results];
- },
-
- /**
- * Retrieves the dom path of the current event.
- *
- * If the event object contains a `path` property, then use it,
- * otherwise, construct the dom path based on the event target.
- *
- * @param {!Event} e
- * @return {string}
- * @example
- *
- * domNode.onclick = e => {
- * getEventPath(e); // eg: div.class1>p#pid.class2
- * }
- */
- getEventPath: e => {
- if (!e) return '';
-
- let path = e.path;
- if (!path || !path.length) {
- path = [];
- let el = e.target;
- while (el) {
- path.push(el);
- el = el.parentNode || el.host;
- }
- }
-
- return path.reduce((domPath, curEl) => {
- const pathForEl = getPathFromNode(curEl);
- if (!pathForEl) return domPath;
- return domPath ? `${pathForEl}>${domPath}` : pathForEl;
- }, '');
- },
};
diff --git a/polygerrit-ui/app/scripts/util_test.html b/polygerrit-ui/app/scripts/util_test.html
deleted file mode 100644
index a3893d2..0000000
--- a/polygerrit-ui/app/scripts/util_test.html
+++ /dev/null
@@ -1,89 +0,0 @@
-<!DOCTYPE html>
-<!--
-@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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
- <template>
- <div id="test" class="a b c">
- <a class="testBtn"></a>
- </div>
- </template>
-</test-fixture>
-
-<script type="module">
- import '../test/common-test-setup.js';
- import {util} from './util.js';
- suite('util tests', () => {
- suite('getEventPath', () => {
- test('empty event', () => {
- assert.equal(util.getEventPath(), '');
- assert.equal(util.getEventPath(null), '');
- assert.equal(util.getEventPath(undefined), '');
- assert.equal(util.getEventPath({}), '');
- });
-
- test('event with fake path', () => {
- assert.equal(util.getEventPath({path: []}), '');
- assert.equal(util.getEventPath({path: [
- {tagName: 'dd'},
- ]}), 'dd');
- });
-
- test('event with fake complicated path', () => {
- assert.equal(util.getEventPath({path: [
- {tagName: 'dd', id: 'test', className: 'a b'},
- {tagName: 'DIV', id: 'test2', className: 'a b c'},
- ]}), 'div#test2.a.b.c>dd#test.a.b');
- });
-
- test('event with fake target', () => {
- const fakeTargetParent2 = {
- tagName: 'DIV', id: 'test2', className: 'a b c',
- };
- const fakeTargetParent1 = {
- parentNode: fakeTargetParent2,
- tagName: 'dd',
- id: 'test',
- className: 'a b',
- };
- const fakeTarget = {tagName: 'SPAN', parentNode: fakeTargetParent1};
- assert.equal(
- util.getEventPath({target: fakeTarget}),
- 'div#test2.a.b.c>dd#test.a.b>span'
- );
- });
-
- test('event with real click', () => {
- const element = fixture('basic');
- const aLink = element.querySelector('a');
- let path;
- aLink.onclick = e => path = util.getEventPath(e);
- MockInteractions.click(aLink);
- assert.equal(
- path,
- 'html>body>test-fixture#basic>div#test.a.b.c>a.testBtn'
- );
- });
- });
- });
-</script>
diff --git a/polygerrit-ui/app/styles/shared-styles.js b/polygerrit-ui/app/styles/shared-styles.js
index f5e048f..3e81761 100644
--- a/polygerrit-ui/app/styles/shared-styles.js
+++ b/polygerrit-ui/app/styles/shared-styles.js
@@ -103,19 +103,19 @@
font-weight: var(--font-weight-normal);
line-height: var(--line-height-small);
}
- h1, .font-h1 {
+ .heading-1 {
font-family: var(--header-font-family);
font-size: var(--font-size-h1);
font-weight: var(--font-weight-h1);
line-height: var(--line-height-h1);
}
- h2, .font-h2 {
+ .heading-2 {
font-family: var(--header-font-family);
font-size: var(--font-size-h2);
font-weight: var(--font-weight-h2);
line-height: var(--line-height-h2);
}
- h3, .font-h3 {
+ .heading-3 {
font-family: var(--header-font-family);
font-size: var(--font-size-h3);
font-weight: var(--font-weight-h3);
diff --git a/polygerrit-ui/app/test/tests.js b/polygerrit-ui/app/test/tests.js
index 6892287..f095302 100644
--- a/polygerrit-ui/app/test/tests.js
+++ b/polygerrit-ui/app/test/tests.js
@@ -41,7 +41,6 @@
'admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html',
'admin/gr-plugin-list/gr-plugin-list_test.html',
'admin/gr-repo-access/gr-repo-access_test.html',
- 'admin/gr-repo-command/gr-repo-command_test.html',
'admin/gr-repo-commands/gr-repo-commands_test.html',
'admin/gr-repo-dashboards/gr-repo-dashboards_test.html',
'admin/gr-repo-detail-list/gr-repo-detail-list_test.html',
@@ -154,7 +153,6 @@
'shared/gr-account-link/gr-account-link_test.html',
'shared/gr-alert/gr-alert_test.html',
'shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html',
- 'shared/gr-autocomplete/gr-autocomplete_test.html',
'shared/gr-avatar/gr-avatar_test.html',
'shared/gr-button/gr-button_test.html',
'shared/gr-change-star/gr-change-star_test.html',
@@ -246,7 +244,6 @@
'gr-group-suggestions-provider/gr-group-suggestions-provider_test.html',
'gr-display-name-utils/gr-display-name-utils_test.html',
'gr-email-suggestions-provider/gr-email-suggestions-provider_test.html',
- 'util_test.html',
];
/* eslint-enable max-len */
for (let file of scripts) {
diff --git a/polygerrit-ui/app/utils/dom-util.js b/polygerrit-ui/app/utils/dom-util.js
new file mode 100644
index 0000000..a9f080f
--- /dev/null
+++ b/polygerrit-ui/app/utils/dom-util.js
@@ -0,0 +1,178 @@
+/**
+ * @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.
+ */
+
+function getPathFromNode(el) {
+ if (!el.tagName || el.tagName === 'GR-APP'
+ || el instanceof DocumentFragment
+ || el instanceof HTMLSlotElement) {
+ return '';
+ }
+ let path = el.tagName.toLowerCase();
+ if (el.id) path += `#${el.id}`;
+ if (el.className) path += `.${el.className.replace(/ /g, '.')}`;
+ return path;
+}
+
+/**
+ * Get computed style value.
+ *
+ * If ShadyCSS is provided, use ShadyCSS api.
+ * If `getComputedStyleValue` is provided on the element, use it.
+ * Otherwise fallback to native method (in polymer 2).
+ *
+ */
+export function getComputedStyleValue(name, el) {
+ let style;
+ if (window.ShadyCSS) {
+ style = ShadyCSS.getComputedStyleValue(el, name);
+ } else if (el.getComputedStyleValue) {
+ style = el.getComputedStyleValue(name);
+ } else {
+ style = getComputedStyle(el).getPropertyValue(name);
+ }
+ return style;
+}
+
+/**
+ * Query selector on a dom element.
+ *
+ * This is shadow DOM compatible, but only works when selector is within
+ * one shadow host, won't work if your selector is crossing
+ * multiple shadow hosts.
+ *
+ */
+export function querySelector(el, selector) {
+ let nodes = [el];
+ let result = null;
+ while (nodes.length) {
+ const node = nodes.pop();
+
+ // Skip if it's an invalid node.
+ if (!node || !node.querySelector) continue;
+
+ // Try find it with native querySelector directly
+ result = node.querySelector(selector);
+
+ if (result) {
+ break;
+ }
+
+ // Add all nodes with shadowRoot and loop through
+ const allShadowNodes = [...node.querySelectorAll('*')]
+ .filter(child => !!child.shadowRoot)
+ .map(child => child.shadowRoot);
+ nodes = nodes.concat(allShadowNodes);
+
+ // Add shadowRoot of current node if has one
+ // as its not included in node.querySelectorAll('*')
+ if (node.shadowRoot) {
+ nodes.push(node.shadowRoot);
+ }
+ }
+ return result;
+}
+
+/**
+ * Query selector all dom elements matching with certain selector.
+ *
+ * This is shadow DOM compatible, but only works when selector is within
+ * one shadow host, won't work if your selector is crossing
+ * multiple shadow hosts.
+ *
+ * Note: this can be very expensive, only use when have to.
+ */
+export function querySelectorAll(el, selector) {
+ let nodes = [el];
+ const results = new Set();
+ while (nodes.length) {
+ const node = nodes.pop();
+
+ if (!node || !node.querySelectorAll) continue;
+
+ // Try find all from regular children
+ [...node.querySelectorAll(selector)]
+ .forEach(el => results.add(el));
+
+ // Add all nodes with shadowRoot and loop through
+ const allShadowNodes = [...node.querySelectorAll('*')]
+ .filter(child => !!child.shadowRoot)
+ .map(child => child.shadowRoot);
+ nodes = nodes.concat(allShadowNodes);
+
+ // Add shadowRoot of current node if has one
+ // as its not included in node.querySelectorAll('*')
+ if (node.shadowRoot) {
+ nodes.push(node.shadowRoot);
+ }
+ }
+ return [...results];
+}
+
+/**
+ * Retrieves the dom path of the current event.
+ *
+ * If the event object contains a `path` property, then use it,
+ * otherwise, construct the dom path based on the event target.
+ *
+ * @param {!Event} e
+ * @return {string}
+ * @example
+ *
+ * domNode.onclick = e => {
+ * getEventPath(e); // eg: div.class1>p#pid.class2
+ * }
+ */
+export function getEventPath(e) {
+ if (!e) return '';
+
+ let path = e.path;
+ if (!path || !path.length) {
+ path = [];
+ let el = e.target;
+ while (el) {
+ path.push(el);
+ el = el.parentNode || el.host;
+ }
+ }
+
+ return path.reduce((domPath, curEl) => {
+ const pathForEl = getPathFromNode(curEl);
+ if (!pathForEl) return domPath;
+ return domPath ? `${pathForEl}>${domPath}` : pathForEl;
+ }, '');
+}
+
+/**
+ * Are any ancestors of the element (or the element itself) members of the
+ * given class.
+ *
+ * @param {!Element} element
+ * @param {string} className
+ * @param {Element=} opt_stopElement If provided, stop traversing the
+ * ancestry when the stop element is reached. The stop element's class
+ * is not checked.
+ * @return {boolean}
+ */
+export function descendedFromClass(element, className, opt_stopElement) {
+ let isDescendant = element.classList.contains(className);
+ while (!isDescendant && element.parentElement &&
+ (!opt_stopElement || element.parentElement !== opt_stopElement)) {
+ isDescendant = element.classList.contains(className);
+ element = element.parentElement;
+ }
+ return isDescendant;
+}
\ No newline at end of file
diff --git a/polygerrit-ui/app/utils/dom-util_test.js b/polygerrit-ui/app/utils/dom-util_test.js
new file mode 100644
index 0000000..c317578
--- /dev/null
+++ b/polygerrit-ui/app/utils/dom-util_test.js
@@ -0,0 +1,139 @@
+/**
+ * @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 {getComputedStyleValue, querySelector, querySelectorAll, descendedFromClass, getEventPath} from './dom-util.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+
+class TestEle extends PolymerElement {
+ static get is() {
+ return 'dom-util-test-element';
+ }
+
+ static get template() {
+ return html`
+ <div>
+ <div class="a">
+ <div class="b">
+ <div class="c"></div>
+ </div>
+ <span class="ss"></span>
+ </div>
+ <span class="ss"></span>
+ </div>
+ `;
+ }
+}
+
+customElements.define(TestEle.is, TestEle);
+
+const basicFixture = fixtureFromTemplate(html`
+ <div id="test" class="a b c">
+ <a class="testBtn" style="color:red;"></a>
+ <dom-util-test-element></dom-util-test-element>
+ <span class="ss"></span>
+ </div>
+`);
+
+suite('dom-util tests', () => {
+ suite('getEventPath', () => {
+ test('empty event', () => {
+ assert.equal(getEventPath(), '');
+ assert.equal(getEventPath(null), '');
+ assert.equal(getEventPath(undefined), '');
+ assert.equal(getEventPath({}), '');
+ });
+
+ test('event with fake path', () => {
+ assert.equal(getEventPath({path: []}), '');
+ assert.equal(getEventPath({path: [
+ {tagName: 'dd'},
+ ]}), 'dd');
+ });
+
+ test('event with fake complicated path', () => {
+ assert.equal(getEventPath({path: [
+ {tagName: 'dd', id: 'test', className: 'a b'},
+ {tagName: 'DIV', id: 'test2', className: 'a b c'},
+ ]}), 'div#test2.a.b.c>dd#test.a.b');
+ });
+
+ test('event with fake target', () => {
+ const fakeTargetParent2 = {
+ tagName: 'DIV', id: 'test2', className: 'a b c',
+ };
+ const fakeTargetParent1 = {
+ parentNode: fakeTargetParent2,
+ tagName: 'dd',
+ id: 'test',
+ className: 'a b',
+ };
+ const fakeTarget = {tagName: 'SPAN', parentNode: fakeTargetParent1};
+ assert.equal(
+ getEventPath({target: fakeTarget}),
+ 'div#test2.a.b.c>dd#test.a.b>span'
+ );
+ });
+
+ test('event with real click', () => {
+ const element = basicFixture.instantiate();
+ const aLink = element.querySelector('a');
+ let path;
+ aLink.onclick = e => path = getEventPath(e);
+ MockInteractions.click(aLink);
+ assert.equal(
+ path,
+ `html>body>test-fixture#${basicFixture.fixtureId}>` +
+ 'div#test.a.b.c>a.testBtn'
+ );
+ });
+ });
+
+ suite('querySelector and querySelectorAll', () => {
+ test('query cross shadow dom', () => {
+ const element = basicFixture.instantiate();
+ const theFirstEl = querySelector(element, '.ss');
+ const allEls = querySelectorAll(element, '.ss');
+ assert.equal(allEls.length, 3);
+ assert.equal(theFirstEl, allEls[0]);
+ });
+ });
+
+ suite('getComputedStyleValue', () => {
+ test('color style', () => {
+ const element = basicFixture.instantiate();
+ const testBtn = querySelector(element, '.testBtn');
+ assert.equal(
+ getComputedStyleValue('color', testBtn), 'rgb(255, 0, 0)'
+ );
+ });
+ });
+
+ suite('descendedFromClass', () => {
+ test('basic tests', () => {
+ const element = basicFixture.instantiate();
+ const testEl = querySelector(element, 'dom-util-test-element');
+ // .c is a child of .a and not vice versa.
+ assert.isTrue(descendedFromClass(querySelector(testEl, '.c'), 'a'));
+ assert.isFalse(descendedFromClass(querySelector(testEl, '.a'), 'c'));
+
+ // Stops at stop element.
+ assert.isFalse(descendedFromClass(querySelector(testEl, '.c'), 'a',
+ querySelector(testEl, '.b')));
+ });
+ });
+});
\ No newline at end of file
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index 821b724..8fb3eea 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -328,11 +328,6 @@
"ba-linkify@file:../../lib/ba-linkify/src":
version "1.0.0"
-es6-promise@^3.3.1:
- version "3.3.1"
- resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
- integrity sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=
-
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
@@ -362,8 +357,3 @@
dependencies:
"@polymer/polymer" "^3.0.2"
"@webcomponents/webcomponentsjs" "^2.0.3"
-
-whatwg-fetch@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
- integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==
diff --git a/resources/com/google/gerrit/httpd/raw/HostPage.html b/resources/com/google/gerrit/httpd/raw/HostPage.html
deleted file mode 100644
index c0d8446..0000000
--- a/resources/com/google/gerrit/httpd/raw/HostPage.html
+++ /dev/null
@@ -1,26 +0,0 @@
-<html>
- <head>
- <title>Gerrit Code Review</title>
- <meta name="gwt:property" content="locale=en_US" />
- <script id="gerrit_hostpagedata"></script>
- <style id="gerrit_sitecss" type="text/css"></style>
- <link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
- </head>
- <body>
- <div id="gerrit_topmenu"></div>
- <div id="gerrit_header"></div>
- <div id="gerrit_startinggerrit" style="margin-left: 10px;">
- <p>Loading <a href="https://www.gerritcodereview.com/" target="_blank">Gerrit Code Review</a> ...</p>
- <noscript>
- <p>Gerrit requires a JavaScript enabled browser.</p>
- </noscript>
- </div>
- <div id="gerrit_body"></div>
- <div style="clear: both">
- <div id="gerrit_footer"></div>
- <div id="gerrit_btmmenu"></div>
- </div>
- <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe>
- <script id="gerrit_module"></script>
- </body>
-</html>
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
index c9ac0fe..2b473bc 100644
--- a/tools/bzl/pkg_war.bzl
+++ b/tools/bzl/pkg_war.bzl
@@ -23,6 +23,7 @@
"//lib/bouncycastle:bcprov",
"//lib/bouncycastle:bcpg",
"//lib/log:impl-log4j",
+ "//lib:jgit-ssh-jsch",
"//prolog:gerrit-prolog-common",
"//resources:log4j-config",
]
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index 61b12f1..d49e700 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -40,7 +40,10 @@
java_binary(
name = "%s__non_stamped" % name,
- deploy_manifest_lines = manifest_entries + ["Gerrit-ApiType: plugin"],
+ deploy_manifest_lines = manifest_entries + [
+ "Gerrit-ApiType: plugin",
+ "Gerrit-ApiVersion: " + GERRIT_VERSION,
+ ],
main_class = "Dummy",
runtime_deps = [
":%s__plugin" % name,
@@ -64,7 +67,7 @@
"GEN_VERSION=$$(cat bazel-out/stable-status.txt | grep -w STABLE_BUILD_%s_LABEL | cut -d ' ' -f 2)" % dir_name.upper(),
"cd $$TMP",
"unzip -q $$ROOT/$<",
- "echo \"Implementation-Version: $$GEN_VERSION\nGerrit-ApiVersion: " + GERRIT_VERSION + "\n$$(cat META-INF/MANIFEST.MF)\" > META-INF/MANIFEST.MF",
+ "echo \"Implementation-Version: $$GEN_VERSION\n$$(cat META-INF/MANIFEST.MF)\" > META-INF/MANIFEST.MF",
"find . -exec touch '{}' ';'",
"zip -Xqr $$ROOT/$@ .",
]),
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index 9915a6e..f360fa5 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -181,6 +181,8 @@
classpathentry('src', 'modules/jgit/org.eclipse.jgit.http.server/src')
classpathentry('src', 'modules/jgit/org.eclipse.jgit.http.server/resources')
classpathentry('src', 'modules/jgit/org.eclipse.jgit.junit/src')
+ classpathentry('src', 'modules/jgit/org.eclipse.jgit.ssh.jsch/src')
+ classpathentry('src', 'modules/jgit/org.eclipse.jgit.ssh.jsch/resources')
def classpathentry(kind, path, src=None, out=None, exported=None, excluding=None):
e = doc.createElement('classpathentry')
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index 0f6f6d4..8748250 100644
--- a/tools/maven/gerrit-acceptance-framework_pom.xml
+++ b/tools/maven/gerrit-acceptance-framework_pom.xml
@@ -44,9 +44,6 @@
<name>Han-Wen Nienhuys</name>
</developer>
<developer>
- <name>Hugo Arès</name>
- </developer>
- <developer>
<name>Luca Milanesio</name>
</developer>
<developer>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index cb0f882..ae31ac9 100644
--- a/tools/maven/gerrit-extension-api_pom.xml
+++ b/tools/maven/gerrit-extension-api_pom.xml
@@ -44,9 +44,6 @@
<name>Han-Wen Nienhuys</name>
</developer>
<developer>
- <name>Hugo Arès</name>
- </developer>
- <developer>
<name>Luca Milanesio</name>
</developer>
<developer>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index e542b47..dc25c80 100644
--- a/tools/maven/gerrit-plugin-api_pom.xml
+++ b/tools/maven/gerrit-plugin-api_pom.xml
@@ -44,9 +44,6 @@
<name>Han-Wen Nienhuys</name>
</developer>
<developer>
- <name>Hugo Arès</name>
- </developer>
- <developer>
<name>Luca Milanesio</name>
</developer>
<developer>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml
index 4a69f2e..d21f88c 100644
--- a/tools/maven/gerrit-war_pom.xml
+++ b/tools/maven/gerrit-war_pom.xml
@@ -44,9 +44,6 @@
<name>Han-Wen Nienhuys</name>
</developer>
<developer>
- <name>Hugo Arès</name>
- </developer>
- <developer>
<name>Luca Milanesio</name>
</developer>
<developer>
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index c37ce12..1da5004 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -96,8 +96,8 @@
# and httpasyncclient as necessary.
maven_jar(
name = "elasticsearch-rest-client",
- artifact = "org.elasticsearch.client:elasticsearch-rest-client:7.7.0",
- sha1 = "5fc25eec3940bc0e9b0ffddcf50554a609e9db8e",
+ artifact = "org.elasticsearch.client:elasticsearch-rest-client:7.7.1",
+ sha1 = "6d44a8e35c11df6883747200bcf46f476a1782b8",
)
maven_jar(
@@ -106,6 +106,29 @@
sha1 = "f84302e14648f9f63c0c73951054aeb2ff0b810a",
)
+ # Google internal dependencies: these are developed at Google, so there is
+ # no concern about version skew.
+
+ FLOGGER_VERS = "0.5.1"
+
+ maven_jar(
+ name = "flogger",
+ artifact = "com.google.flogger:flogger:" + FLOGGER_VERS,
+ sha1 = "71d1e2cef9cc604800825583df56b8ef5c053f14",
+ )
+
+ maven_jar(
+ name = "flogger-log4j-backend",
+ artifact = "com.google.flogger:flogger-log4j-backend:" + FLOGGER_VERS,
+ sha1 = "5e2794b75c88223f263f1c1a9d7ea51e2dc45732",
+ )
+
+ maven_jar(
+ name = "flogger-system-backend",
+ artifact = "com.google.flogger:flogger-system-backend:" + FLOGGER_VERS,
+ sha1 = "b66d3bedb14da604828a8693bb24fd78e36b0e9e",
+ )
+
# Test-only dependencies below.
maven_jar(
@@ -120,18 +143,18 @@
sha1 = "dc13ae4faca6df981fc7aeb5a522d9db446d5d50",
)
- TESTCONTAINERS_VERSION = "1.14.2"
+ TESTCONTAINERS_VERSION = "1.14.3"
maven_jar(
name = "testcontainers",
artifact = "org.testcontainers:testcontainers:" + TESTCONTAINERS_VERSION,
- sha1 = "d74bc045fb5f30988b0adff20244412972a9f564",
+ sha1 = "071fc82ba663f469447a19434e7db90f3a872753",
)
maven_jar(
name = "testcontainers-elasticsearch",
artifact = "org.testcontainers:elasticsearch:" + TESTCONTAINERS_VERSION,
- sha1 = "66e1a6da0362beee83673b877c9c2e0536d6912c",
+ sha1 = "3709e2ebb0b6aa4e2ba2b6ca92ffdd3bf637a86c",
)
maven_jar(