Merge "A11y - Keyboard focus moves only to interactive elements on Dashboard"
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 47afdba..8509b1f 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -1425,8 +1425,7 @@
any of their groups is used.
This limit applies not only to the link:cmd-query.html[`gerrit query`]
-command, but also to the web UI results pagination size in the new
-PolyGerrit UI and, limited to the full project list, in the old GWT UI.
+command, but also to the web UI results pagination size.
[[capability_readAs]]
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 8ad8669..1f4fd9c 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -828,6 +828,49 @@
+
If 0 or negative, disk storage for the cache is disabled.
+[[cache.name.expireAfterWrite]]cache.<name>.expireAfterWrite::
++
+Duration after which a cached value will be evicted and not
+read anymore.
++
+Values should use common unit suffixes to express their setting:
++
+* ms, milliseconds
+* s, sec, second, seconds
+* m, min, minute, minutes
+* h, hr, hour, hours
++
+Disabled by default.
+
+[[cache.name.refreshAfterWrite]]cache.<name>.refreshAfterWrite::
++
+Duration after which we asynchronously refresh the cached value.
++
+Values should use common unit suffixes to express their setting:
++
+* ms, milliseconds
+* s, sec, second, seconds
+* m, min, minute, minutes
+* h, hr, hour, hours
++
+This applies only to these caches that support refreshing:
++
+* `"projects"`: Caching project information in-memory. Defaults to 15 minutes.
+
+[[cache.refreshThreadPoolSize]]cache.refreshThreadPoolSize::
++
+Number of threads that are available to refresh cached values that became
+out of date. This applies only to these caches that support refreshing:
++
+* `"projects"`: Caching project information in-memory
++
+Refreshes will only be scheduled on this executor if the values are
+out of sync.
+The check if they are is cheap and always happens on the thread that
+inquires for a cached value.
++
+Defaults to 2.
+
==== [[cache_names]]Standard Caches
cache `"accounts"`::
@@ -1125,22 +1168,6 @@
+
Default is true, enabled.
-[[cache.projects.checkFrequency]]cache.projects.checkFrequency::
-+
-How often project configuration should be checked for update from Git.
-Gerrit Code Review caches project access rules and configuration in
-memory, checking the refs/meta/config branch every checkFrequency
-minutes to see if a new revision should be loaded and used for future
-access. Values can be specified using standard time unit abbreviations
-('ms', 'sec', 'min', etc.).
-+
-If set to 0, checks occur every time, which may slow down operations.
-If set to 'disabled' or 'off', no check will ever be done.
-Administrators may force the cache to flush with
-link:cmd-flush-caches.html[gerrit flush-caches].
-+
-Default is 5 minutes.
-
[[cache.projects.loadOnStartup]]cache.projects.loadOnStartup::
+
If the project cache should be loaded during server startup.
@@ -2215,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.
@@ -2238,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::
+
@@ -2919,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.
@@ -3736,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
@@ -5347,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/metrics.txt b/Documentation/metrics.txt
index 7e6799be..3040348 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -64,6 +64,7 @@
* `caches/memory_eviction_count`: Memory eviction count.
* `caches/disk_cached`: Disk entries used by persistent cache.
* `caches/disk_hit_ratio`: Disk hit ratio for persistent cache.
+* `caches/refresh_count`: The number of refreshes per cache with an indicator if a reload was necessary.
=== Change
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 555f5b1..91eef76 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -60,8 +60,8 @@
http_archive(
name = "build_bazel_rules_nodejs",
- sha256 = "d0c4bb8b902c1658f42eb5563809c70a06e46015d64057d25560b0eb4bdc9007",
- urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/1.5.0/rules_nodejs-1.5.0.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/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ApproveChange-body.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ApproveChange-body.json
new file mode 100644
index 0000000..670aa9f
--- /dev/null
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ApproveChange-body.json
@@ -0,0 +1,5 @@
+{
+ "labels": {
+ "Code-Review": 2
+ }
+}
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ApproveChange.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ApproveChange.json
new file mode 100644
index 0000000..3577a6a
--- /dev/null
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/ApproveChange.json
@@ -0,0 +1,6 @@
+[
+ {
+ "url": "http://HOSTNAME:HTTP_PORT/a/changes/",
+ "number": "NUMBER"
+ }
+]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateChange.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateChange.json
index c267ab3..b4ee549 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateChange.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/CreateChange.json
@@ -1,6 +1,6 @@
[
{
"url": "http://HOSTNAME:HTTP_PORT/a/changes/",
- "project": "_PROJECT"
+ "project": "PROJECT"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteChange.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteChange.json
index 53b947a..3577a6a 100644
--- a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteChange.json
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/DeleteChange.json
@@ -1,6 +1,6 @@
[
{
"url": "http://HOSTNAME:HTTP_PORT/a/changes/",
- "number": "_NUMBER"
+ "number": "NUMBER"
}
]
diff --git a/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/SubmitChange.json b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/SubmitChange.json
new file mode 100644
index 0000000..a371757
--- /dev/null
+++ b/e2e-tests/src/test/resources/data/com/google/gerrit/scenarios/SubmitChange.json
@@ -0,0 +1,5 @@
+[
+ {
+ "url": "http://HOSTNAME:HTTP_PORT/a/changes/"
+ }
+]
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ApproveChange.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ApproveChange.scala
new file mode 100644
index 0000000..fe46bd6
--- /dev/null
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ApproveChange.scala
@@ -0,0 +1,48 @@
+// 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.scenarios
+
+import io.gatling.core.Predef.{atOnceUsers, _}
+import io.gatling.core.feeder.FeederBuilder
+import io.gatling.core.structure.ScenarioBuilder
+import io.gatling.http.Predef.http
+
+class ApproveChange extends GerritSimulation {
+ private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
+ private var createChange: Option[CreateChange] = None
+
+ def this(createChange: CreateChange) {
+ this()
+ this.createChange = Some(createChange)
+ }
+
+ val test: ScenarioBuilder = scenario(unique)
+ .feed(data)
+ .exec(session => {
+ if (createChange.nonEmpty) {
+ session.set("number", createChange.get.number)
+ } else {
+ session
+ }
+ })
+ .exec(http(unique)
+ .post("${url}${number}/revisions/current/review")
+ .body(ElFileBody(body)).asJson)
+
+ setUp(
+ test.inject(
+ atOnceUsers(1)
+ )).protocols(httpProtocol)
+}
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateChange.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateChange.scala
index 57e6bcd..c7fb8ed 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateChange.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/CreateChange.scala
@@ -21,26 +21,31 @@
import scala.concurrent.duration._
-class CreateChange extends GerritSimulation {
+class CreateChange extends ProjectSimulation {
private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
- private val default: String = name
private val numberKey = "_number"
+ var number = 0
override def relativeRuntimeWeight = 2
- private val test: ScenarioBuilder = scenario(unique)
+ def this(default: String) {
+ this()
+ this.default = default
+ }
+
+ val test: ScenarioBuilder = scenario(unique)
.feed(data)
.exec(httpRequest
.body(ElFileBody(body)).asJson
.check(regex("\"" + numberKey + "\":(\\d+),").saveAs(numberKey)))
.exec(session => {
- deleteChange.number = Some(session(numberKey).as[Int])
+ number = session(numberKey).as[Int]
session
})
private val createProject = new CreateProject(default)
private val deleteProject = new DeleteProject(default)
- private val deleteChange = new DeleteChange
+ private val deleteChange = new DeleteChange(this)
setUp(
createProject.test.inject(
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteChange.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteChange.scala
index 1b3bbc1..aa6fe0d 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteChange.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/DeleteChange.scala
@@ -21,15 +21,20 @@
class DeleteChange extends GerritSimulation {
private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
- var number: Option[Int] = None
+ private var createChange: Option[CreateChange] = None
override def relativeRuntimeWeight = 2
+ def this(createChange: CreateChange) {
+ this()
+ this.createChange = Some(createChange)
+ }
+
val test: ScenarioBuilder = scenario(unique)
.feed(data)
.exec(session => {
- if (number.nonEmpty) {
- session.set("number", number.get)
+ if (createChange.nonEmpty) {
+ session.set("number", createChange.get.number)
} else {
session
}
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala
index b427c0d..5d6176d 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GerritSimulation.scala
@@ -66,7 +66,8 @@
val precedes = replaceKeyWith("_number", 0, number.toString)
replaceProperty("number", 1, precedes)
case ("project", project) =>
- val precedes = replaceKeyWith("_project", name, project.toString)
+ var precedes = replaceKeyWith("_project", name, project.toString)
+ precedes = replaceOverride(precedes)
replaceProperty("project", precedes)
case ("entries", entries) =>
replaceProperty("projects_entries", "1", entries.toString)
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/SubmitChange.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/SubmitChange.scala
new file mode 100644
index 0000000..2f67274
--- /dev/null
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/SubmitChange.scala
@@ -0,0 +1,62 @@
+// 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.scenarios
+
+import io.gatling.core.Predef.{atOnceUsers, _}
+import io.gatling.core.feeder.FeederBuilder
+import io.gatling.core.structure.ScenarioBuilder
+import io.gatling.http.Predef.http
+
+import scala.concurrent.duration._
+
+class SubmitChange extends GerritSimulation {
+ private val data: FeederBuilder = jsonFile(resource).convert(keys).queue
+ private val default: String = name
+
+ private val test: ScenarioBuilder = scenario(unique)
+ .feed(data)
+ .exec(session => {
+ session.set("number", createChange.number)
+ })
+ .exec(http(unique).post("${url}${number}/submit"))
+
+ private val createProject = new CreateProject(default)
+ private val createChange = new CreateChange(default)
+ private val approveChange = new ApproveChange(createChange)
+ private val deleteProject = new DeleteProject(default)
+
+ setUp(
+ createProject.test.inject(
+ nothingFor(stepWaitTime(createProject) seconds),
+ atOnceUsers(1)
+ ),
+ createChange.test.inject(
+ nothingFor(stepWaitTime(createChange) seconds),
+ atOnceUsers(1)
+ ),
+ approveChange.test.inject(
+ nothingFor(stepWaitTime(approveChange) seconds),
+ atOnceUsers(1)
+ ),
+ test.inject(
+ nothingFor(stepWaitTime(this) seconds),
+ atOnceUsers(1)
+ ),
+ deleteProject.test.inject(
+ nothingFor(stepWaitTime(deleteProject) seconds),
+ atOnceUsers(1)
+ ),
+ ).protocols(httpProtocol)
+}
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/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 0ef6ad5..2d62608 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -483,7 +483,6 @@
cfg.setString("gerrit", null, "basePath", "git");
cfg.setBoolean("sendemail", null, "enable", true);
cfg.setInt("sendemail", null, "threadPoolSize", 0);
- cfg.setInt("cache", "projects", "checkFrequency", 0);
cfg.setInt("plugins", null, "checkFrequency", 0);
cfg.setInt("sshd", null, "threads", 1);
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/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 05992d4..0c3b7b0 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -246,7 +246,7 @@
}
private boolean sshdOff() {
- return new SshAddressesModule().getListenAddresses(config).isEmpty();
+ return new SshAddressesModule().provideListenAddresses(config).isEmpty();
}
private Injector createCfgInjector() {
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/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 034e042e..57bec71 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -380,7 +380,7 @@
}
private boolean sshdOff() {
- return new SshAddressesModule().getListenAddresses(config).isEmpty();
+ return new SshAddressesModule().provideListenAddresses(config).isEmpty();
}
private String myVersion() {
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/CacheRefreshExecutor.java b/java/com/google/gerrit/server/CacheRefreshExecutor.java
new file mode 100644
index 0000000..1a377c3
--- /dev/null
+++ b/java/com/google/gerrit/server/CacheRefreshExecutor.java
@@ -0,0 +1,28 @@
+// 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;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+/**
+ * Marker on the global {@link java.util.concurrent.ThreadPoolExecutor} used to refresh outdated
+ * values in caches.
+ */
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface CacheRefreshExecutor {}
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/PublishCommentsOp.java b/java/com/google/gerrit/server/PublishCommentsOp.java
index df57629..745755b 100644
--- a/java/com/google/gerrit/server/PublishCommentsOp.java
+++ b/java/com/google/gerrit/server/PublishCommentsOp.java
@@ -19,6 +19,7 @@
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.server.change.EmailReviewComments;
@@ -31,6 +32,7 @@
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.CommentsRejectedException;
import com.google.gerrit.server.update.Context;
+import com.google.gerrit.server.update.RepoView;
import com.google.gerrit.server.util.LabelVote;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -114,7 +116,16 @@
PatchSet ps = psUtil.get(changeNotes, psId);
NotifyResolver.Result notify = ctx.getNotify(changeNotes.getChangeId());
if (notify.shouldNotify()) {
- email.create(notify, changeNotes, ps, user, message, comments, null, labelDelta).sendAsync();
+ RepoView repoView;
+ try {
+ repoView = ctx.getRepoView();
+ } catch (IOException ex) {
+ throw new StorageException(
+ String.format("Repository %s not found", ctx.getProject().get()), ex);
+ }
+ email
+ .create(notify, changeNotes, ps, user, message, comments, null, labelDelta, repoView)
+ .sendAsync();
}
commentAdded.fire(
changeNotes.getChange(),
diff --git a/java/com/google/gerrit/server/account/AccountManager.java b/java/com/google/gerrit/server/account/AccountManager.java
index 345da81..7a5b1aa 100644
--- a/java/com/google/gerrit/server/account/AccountManager.java
+++ b/java/com/google/gerrit/server/account/AccountManager.java
@@ -18,6 +18,7 @@
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
@@ -78,8 +79,9 @@
private final boolean autoUpdateAccountActiveStatus;
private final SetInactiveFlag setInactiveFlag;
+ @VisibleForTesting
@Inject
- AccountManager(
+ public AccountManager(
Sequences sequences,
@GerritServerConfig Config cfg,
Accounts accounts,
@@ -239,13 +241,7 @@
if (!Strings.isNullOrEmpty(who.getDisplayName())
&& !Objects.equals(user.getAccount().fullName(), who.getDisplayName())) {
- if (realm.allowsEdit(AccountFieldName.FULL_NAME)) {
- accountUpdates.add(a -> a.setFullName(who.getDisplayName()));
- } else {
- logger.atWarning().log(
- "Not changing already set display name '%s' to '%s'",
- user.getAccount().fullName(), who.getDisplayName());
- }
+ accountUpdates.add(a -> a.setFullName(who.getDisplayName()));
}
if (!realm.allowsEdit(AccountFieldName.USER_NAME)
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/auth/ldap/LdapRealm.java b/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index c53ba83..1421f17 100644
--- a/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -218,7 +218,7 @@
values.put(name, m.get(name));
}
- String r = p.replace(values);
+ String r = p.replace(values).trim();
return r.isEmpty() ? null : r;
}
diff --git a/java/com/google/gerrit/server/cache/CacheBinding.java b/java/com/google/gerrit/server/cache/CacheBinding.java
index 9d90d073..99db64e 100644
--- a/java/com/google/gerrit/server/cache/CacheBinding.java
+++ b/java/com/google/gerrit/server/cache/CacheBinding.java
@@ -29,6 +29,13 @@
/** Set the time an element lives after last access before being expired. */
CacheBinding<K, V> expireFromMemoryAfterAccess(Duration duration);
+ /**
+ * Set the time that an element will be refreshed after. Elements older than this but younger than
+ * {@link #expireAfterWrite(Duration)} will still be returned, but on access a task is queued to
+ * refresh their value asynchronously.
+ */
+ CacheBinding<K, V> refreshAfterWrite(Duration duration);
+
/** Populate the cache with items from the CacheLoader. */
CacheBinding<K, V> loader(Class<? extends CacheLoader<K, V>> clazz);
diff --git a/java/com/google/gerrit/server/cache/CacheDef.java b/java/com/google/gerrit/server/cache/CacheDef.java
index d0c633e..31a453e 100644
--- a/java/com/google/gerrit/server/cache/CacheDef.java
+++ b/java/com/google/gerrit/server/cache/CacheDef.java
@@ -51,6 +51,9 @@
Duration expireFromMemoryAfterAccess();
@Nullable
+ Duration refreshAfterWrite();
+
+ @Nullable
Weigher<K, V> weigher();
@Nullable
diff --git a/java/com/google/gerrit/server/cache/CacheProvider.java b/java/com/google/gerrit/server/cache/CacheProvider.java
index fe4244c..2dd9e1f 100644
--- a/java/com/google/gerrit/server/cache/CacheProvider.java
+++ b/java/com/google/gerrit/server/cache/CacheProvider.java
@@ -38,6 +38,7 @@
private long maximumWeight;
private Duration expireAfterWrite;
private Duration expireFromMemoryAfterAccess;
+ private Duration refreshAfterWrite;
private Provider<CacheLoader<K, V>> loader;
private Provider<Weigher<K, V>> weigher;
@@ -90,6 +91,13 @@
}
@Override
+ public CacheBinding<K, V> refreshAfterWrite(Duration duration) {
+ checkNotFrozen();
+ refreshAfterWrite = duration;
+ return this;
+ }
+
+ @Override
public CacheBinding<K, V> loader(Class<? extends CacheLoader<K, V>> impl) {
checkNotFrozen();
loader = module.bindCacheLoader(this, impl);
@@ -151,6 +159,11 @@
}
@Override
+ public Duration refreshAfterWrite() {
+ return refreshAfterWrite;
+ }
+
+ @Override
@Nullable
public Weigher<K, V> weigher() {
return weigher != null ? weigher.get() : null;
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java b/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
index 5d9ce60..aa62745 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheDefProxy.java
@@ -43,6 +43,11 @@
}
@Override
+ public Duration refreshAfterWrite() {
+ return source.refreshAfterWrite();
+ }
+
+ @Override
public Weigher<K, V> weigher() {
Weigher<K, V> weigher = source.weigher();
if (weigher == null) {
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index 8f7e360..82615a4 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -237,6 +237,7 @@
def.valueSerializer(),
def.version(),
maxSize,
- def.expireAfterWrite());
+ def.expireAfterWrite(),
+ def.expireFromMemoryAfterAccess());
}
}
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index ef4e44c..7a53600 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -23,6 +23,9 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.common.hash.BloomFilter;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.server.cache.PersistentCache;
import com.google.gerrit.server.cache.serialize.CacheSerializer;
@@ -40,6 +43,7 @@
import java.sql.Statement;
import java.sql.Timestamp;
import java.time.Duration;
+import java.time.Instant;
import java.util.Calendar;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
@@ -122,7 +126,12 @@
@Override
public V get(K key) throws ExecutionException {
if (mem instanceof LoadingCache) {
- return ((LoadingCache<K, ValueHolder<V>>) mem).get(key).value;
+ LoadingCache<K, ValueHolder<V>> asLoadingCache = (LoadingCache<K, ValueHolder<V>>) mem;
+ ValueHolder<V> valueHolder = asLoadingCache.get(key);
+ if (store.needsRefresh(valueHolder.created)) {
+ asLoadingCache.refresh(key);
+ }
+ return valueHolder.value;
}
throw new UnsupportedOperationException();
}
@@ -139,8 +148,8 @@
}
}
- ValueHolder<V> h = new ValueHolder<>(valueLoader.call());
- h.created = TimeUtil.nowMs();
+ ValueHolder<V> h =
+ new ValueHolder<>(valueLoader.call(), Instant.ofEpochMilli(TimeUtil.nowMs()));
executor.execute(() -> store.put(key, h));
return h;
})
@@ -149,8 +158,7 @@
@Override
public void put(K key, V val) {
- final ValueHolder<V> h = new ValueHolder<>(val);
- h.created = TimeUtil.nowMs();
+ final ValueHolder<V> h = new ValueHolder<>(val, Instant.ofEpochMilli(TimeUtil.nowMs()));
mem.put(key, h);
executor.execute(() -> store.put(key, h));
}
@@ -217,11 +225,12 @@
static class ValueHolder<V> {
final V value;
- long created;
+ final Instant created;
volatile boolean clean;
- ValueHolder(V value) {
+ ValueHolder(V value, Instant created) {
this.value = value;
+ this.created = created;
}
}
@@ -248,12 +257,34 @@
}
}
- final ValueHolder<V> h = new ValueHolder<>(loader.load(key));
- h.created = TimeUtil.nowMs();
+ final ValueHolder<V> h =
+ new ValueHolder<>(loader.load(key), Instant.ofEpochMilli(TimeUtil.nowMs()));
executor.execute(() -> store.put(key, h));
return h;
}
}
+
+ @Override
+ public ListenableFuture<ValueHolder<V>> reload(K key, ValueHolder<V> oldValue)
+ throws Exception {
+ ListenableFuture<V> reloadedValue = loader.reload(key, oldValue.value);
+ Futures.addCallback(
+ reloadedValue,
+ new FutureCallback<V>() {
+ @Override
+ public void onSuccess(V result) {
+ store.put(key, new ValueHolder<>(result, TimeUtil.now()));
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ logger.atWarning().withCause(t).log("Unable to reload cache value");
+ }
+ },
+ executor);
+
+ return Futures.transform(reloadedValue, v -> new ValueHolder<>(v, TimeUtil.now()), executor);
+ }
}
static class SqlStore<K, V> {
@@ -263,6 +294,7 @@
private final int version;
private final long maxSize;
@Nullable private final Duration expireAfterWrite;
+ @Nullable private final Duration refreshAfterWrite;
private final BlockingQueue<SqlHandle> handles;
private final AtomicLong hitCount = new AtomicLong();
private final AtomicLong missCount = new AtomicLong();
@@ -276,13 +308,15 @@
CacheSerializer<V> valueSerializer,
int version,
long maxSize,
- @Nullable Duration expireAfterWrite) {
+ @Nullable Duration expireAfterWrite,
+ @Nullable Duration refreshAfterWrite) {
this.url = jdbcUrl;
this.keyType = createKeyType(keyType, keySerializer);
this.valueSerializer = valueSerializer;
this.version = version;
this.maxSize = maxSize;
this.expireAfterWrite = expireAfterWrite;
+ this.refreshAfterWrite = refreshAfterWrite;
int cores = Runtime.getRuntime().availableProcessors();
int keep = Math.min(cores, 16);
@@ -394,14 +428,14 @@
}
Timestamp created = r.getTimestamp(2);
- if (expired(created)) {
+ if (expired(created.toInstant())) {
invalidate(key);
missCount.incrementAndGet();
return null;
}
V val = valueSerializer.deserialize(r.getBytes(1));
- ValueHolder<V> h = new ValueHolder<>(val);
+ ValueHolder<V> h = new ValueHolder<>(val, created.toInstant());
h.clean = true;
hitCount.incrementAndGet();
touch(c, key);
@@ -429,14 +463,22 @@
return false;
}
- private boolean expired(Timestamp created) {
+ private boolean expired(Instant created) {
if (expireAfterWrite == null) {
return false;
}
- Duration age = Duration.between(created.toInstant(), TimeUtil.now());
+ Duration age = Duration.between(created, TimeUtil.now());
return age.compareTo(expireAfterWrite) > 0;
}
+ private boolean needsRefresh(Instant created) {
+ if (refreshAfterWrite == null) {
+ return false;
+ }
+ Duration age = Duration.between(created, TimeUtil.now());
+ return age.compareTo(refreshAfterWrite) > 0;
+ }
+
private void touch(SqlHandle c, K key) throws IOException, SQLException {
if (c.touch == null) {
c.touch = c.conn.prepareStatement("UPDATE data SET accessed=? WHERE k=? AND version=?");
@@ -474,7 +516,7 @@
keyType.set(c.put, 1, key);
c.put.setBytes(2, valueSerializer.serialize(holder.value));
c.put.setInt(3, version);
- c.put.setTimestamp(4, new Timestamp(holder.created));
+ c.put.setTimestamp(4, Timestamp.from(holder.created));
c.put.setTimestamp(5, TimeUtil.nowTs());
c.put.executeUpdate();
holder.clean = true;
@@ -560,7 +602,7 @@
while (maxSize < used && r.next()) {
K key = keyType.get(r, 1);
Timestamp created = r.getTimestamp(3);
- if (mem.getIfPresent(key) != null && !expired(created)) {
+ if (mem.getIfPresent(key) != null && !expired(created.toInstant())) {
touch(c, key);
} else {
invalidate(c, key);
diff --git a/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java b/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
index 9906b3d..23caca7 100644
--- a/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
+++ b/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
@@ -105,6 +105,21 @@
builder.expireAfterAccess(expireAfterAccess.toNanos(), NANOSECONDS);
}
+ Duration refreshAfterWrite = def.refreshAfterWrite();
+ if (has(def.configKey(), "refreshAfterWrite")) {
+ builder.refreshAfterWrite(
+ ConfigUtil.getTimeUnit(
+ cfg,
+ "cache",
+ def.configKey(),
+ "refreshAfterWrite",
+ toSeconds(refreshAfterWrite),
+ SECONDS),
+ SECONDS);
+ } else if (refreshAfterWrite != null) {
+ builder.refreshAfterWrite(refreshAfterWrite.toNanos(), NANOSECONDS);
+ }
+
return builder;
}
@@ -141,6 +156,21 @@
builder.expireAfterAccess(expireAfterAccess.toNanos(), NANOSECONDS);
}
+ Duration refreshAfterWrite = def.refreshAfterWrite();
+ if (has(def.configKey(), "refreshAfterWrite")) {
+ builder.expireAfterAccess(
+ ConfigUtil.getTimeUnit(
+ cfg,
+ "cache",
+ def.configKey(),
+ "refreshAfterWrite",
+ toSeconds(refreshAfterWrite),
+ SECONDS),
+ SECONDS);
+ } else if (refreshAfterWrite != null) {
+ builder.refreshAfterWrite(refreshAfterWrite.toNanos(), NANOSECONDS);
+ }
+
return builder;
}
diff --git a/java/com/google/gerrit/server/change/AbandonOp.java b/java/com/google/gerrit/server/change/AbandonOp.java
index eb6e8d7..9e228d9 100644
--- a/java/com/google/gerrit/server/change/AbandonOp.java
+++ b/java/com/google/gerrit/server/change/AbandonOp.java
@@ -27,6 +27,7 @@
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.extensions.events.ChangeAbandoned;
import com.google.gerrit.server.mail.send.AbandonedSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.mail.send.ReplyToChangeSender;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
@@ -42,6 +43,7 @@
private final ChangeMessagesUtil cmUtil;
private final PatchSetUtil psUtil;
private final ChangeAbandoned changeAbandoned;
+ private final MessageIdGenerator messageIdGenerator;
private final String msgTxt;
private final AccountState accountState;
@@ -61,12 +63,14 @@
ChangeMessagesUtil cmUtil,
PatchSetUtil psUtil,
ChangeAbandoned changeAbandoned,
+ MessageIdGenerator messageIdGenerator,
@Assisted @Nullable AccountState accountState,
@Assisted @Nullable String msgTxt) {
this.abandonedSenderFactory = abandonedSenderFactory;
this.cmUtil = cmUtil;
this.psUtil = psUtil;
this.changeAbandoned = changeAbandoned;
+ this.messageIdGenerator = messageIdGenerator;
this.accountState = accountState;
this.msgTxt = Strings.nullToEmpty(msgTxt);
@@ -116,6 +120,7 @@
}
cm.setChangeMessage(message.getMessage(), ctx.getWhen());
cm.setNotify(notify);
+ cm.setMessageId(messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSet.id()));
cm.send();
} catch (Exception e) {
logger.atSevere().withCause(e).log("Cannot email update for change %s", change.getId());
diff --git a/java/com/google/gerrit/server/change/AddReviewersEmail.java b/java/com/google/gerrit/server/change/AddReviewersEmail.java
index 2778bdd..ae3851e 100644
--- a/java/com/google/gerrit/server/change/AddReviewersEmail.java
+++ b/java/com/google/gerrit/server/change/AddReviewersEmail.java
@@ -25,6 +25,7 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.SendEmailExecutor;
import com.google.gerrit.server.mail.send.AddReviewerSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.Collection;
@@ -37,13 +38,16 @@
private final AddReviewerSender.Factory addReviewerSenderFactory;
private final ExecutorService sendEmailsExecutor;
+ private final MessageIdGenerator messageIdGenerator;
@Inject
AddReviewersEmail(
AddReviewerSender.Factory addReviewerSenderFactory,
- @SendEmailExecutor ExecutorService sendEmailsExecutor) {
+ @SendEmailExecutor ExecutorService sendEmailsExecutor,
+ MessageIdGenerator messageIdGenerator) {
this.addReviewerSenderFactory = addReviewerSenderFactory;
this.sendEmailsExecutor = sendEmailsExecutor;
+ this.messageIdGenerator = messageIdGenerator;
}
public void emailReviewersAsync(
@@ -86,6 +90,9 @@
cm.addReviewersByEmail(immutableAddedByEmail);
cm.addExtraCC(immutableToCopy);
cm.addExtraCCByEmail(immutableCopiedByEmail);
+ cm.setMessageId(
+ messageIdGenerator.fromChangeUpdate(
+ change.getProject(), change.currentPatchSetId()));
cm.send();
} catch (Exception err) {
logger.atSevere().withCause(err).log(
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index bbb94ea..467c4a2 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -59,6 +59,7 @@
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.mail.send.CreateChangeSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.permissions.PermissionBackend;
@@ -108,6 +109,7 @@
private final RevisionCreated revisionCreated;
private final CommentAdded commentAdded;
private final ReviewerAdder reviewerAdder;
+ private final MessageIdGenerator messageIdGenerator;
private final Change.Id changeId;
private final PatchSet.Id psId;
@@ -156,6 +158,7 @@
CommentAdded commentAdded,
RevisionCreated revisionCreated,
ReviewerAdder reviewerAdder,
+ MessageIdGenerator messageIdGenerator,
@Assisted Change.Id changeId,
@Assisted ObjectId commitId,
@Assisted String refName) {
@@ -171,6 +174,7 @@
this.revisionCreated = revisionCreated;
this.commentAdded = commentAdded;
this.reviewerAdder = reviewerAdder;
+ this.messageIdGenerator = messageIdGenerator;
this.changeId = changeId;
this.psId = PatchSet.id(changeId, INITIAL_PATCH_SET_ID);
@@ -475,6 +479,8 @@
cm.addExtraCC(reviewerAdditions.flattenResults(AddReviewersOp.Result::addedCCs));
cm.addExtraCCByEmail(
reviewerAdditions.flattenResults(AddReviewersOp.Result::addedCCsByEmail));
+ cm.setMessageId(
+ messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSet.id()));
cm.send();
} catch (Exception e) {
logger.atSevere().withCause(e).log(
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java b/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
index 3bc9324..e52375f 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
@@ -21,6 +21,7 @@
import com.google.gerrit.mail.Address;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.mail.send.DeleteReviewerSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.Context;
@@ -36,6 +37,8 @@
}
private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
+ private final MessageIdGenerator messageIdGenerator;
+
private final Address reviewer;
private ChangeMessage changeMessage;
@@ -43,8 +46,11 @@
@Inject
DeleteReviewerByEmailOp(
- DeleteReviewerSender.Factory deleteReviewerSenderFactory, @Assisted Address reviewer) {
+ DeleteReviewerSender.Factory deleteReviewerSenderFactory,
+ MessageIdGenerator messageIdGenerator,
+ @Assisted Address reviewer) {
this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
+ this.messageIdGenerator = messageIdGenerator;
this.reviewer = reviewer;
}
@@ -79,6 +85,8 @@
cm.addReviewersByEmail(Collections.singleton(reviewer));
cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
cm.setNotify(notify);
+ cm.setMessageId(
+ messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
cm.send();
} catch (Exception err) {
logger.atSevere().withCause(err).log("Cannot email update for change %s", change.getId());
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerOp.java b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
index 099334d..5a9fb99 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -38,6 +38,7 @@
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.extensions.events.ReviewerDeleted;
import com.google.gerrit.server.mail.send.DeleteReviewerSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectCache;
@@ -45,6 +46,7 @@
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.Context;
+import com.google.gerrit.server.update.RepoView;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
@@ -69,6 +71,7 @@
private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
private final RemoveReviewerControl removeReviewerControl;
private final ProjectCache projectCache;
+ private final MessageIdGenerator messageIdGenerator;
private final AccountState reviewer;
private final DeleteReviewerInput input;
@@ -90,6 +93,7 @@
DeleteReviewerSender.Factory deleteReviewerSenderFactory,
RemoveReviewerControl removeReviewerControl,
ProjectCache projectCache,
+ MessageIdGenerator messageIdGenerator,
@Assisted AccountState reviewerAccount,
@Assisted DeleteReviewerInput input) {
this.approvalsUtil = approvalsUtil;
@@ -101,6 +105,7 @@
this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
this.removeReviewerControl = removeReviewerControl;
this.projectCache = projectCache;
+ this.messageIdGenerator = messageIdGenerator;
this.reviewer = reviewerAccount;
this.input = input;
}
@@ -177,7 +182,7 @@
}
try {
if (notify.shouldNotify()) {
- emailReviewers(ctx.getProject(), currChange, changeMessage, notify);
+ emailReviewers(ctx.getProject(), currChange, changeMessage, notify, ctx.getRepoView());
}
} catch (Exception err) {
logger.atSevere().withCause(err).log("Cannot email update for change %s", currChange.getId());
@@ -211,7 +216,8 @@
Project.NameKey projectName,
Change change,
ChangeMessage changeMessage,
- NotifyResolver.Result notify)
+ NotifyResolver.Result notify,
+ RepoView repoView)
throws EmailException {
Account.Id userId = user.get().getAccountId();
if (userId.equals(reviewer.account().id())) {
@@ -223,6 +229,7 @@
cm.addReviewers(Collections.singleton(reviewer.account().id()));
cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
cm.setNotify(notify);
+ cm.setMessageId(messageIdGenerator.fromChangeUpdate(repoView, change.currentPatchSetId()));
cm.send();
}
}
diff --git a/java/com/google/gerrit/server/change/EmailReviewComments.java b/java/com/google/gerrit/server/change/EmailReviewComments.java
index f7e45e7..f1eff6f 100644
--- a/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -25,8 +25,10 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.SendEmailExecutor;
import com.google.gerrit.server.mail.send.CommentSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.update.RepoView;
import com.google.gerrit.server.util.LabelVote;
import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
@@ -65,13 +67,15 @@
ChangeMessage message,
List<Comment> comments,
String patchSetComment,
- List<LabelVote> labels);
+ List<LabelVote> labels,
+ RepoView repoView);
}
private final ExecutorService sendEmailsExecutor;
private final PatchSetInfoFactory patchSetInfoFactory;
private final CommentSender.Factory commentSenderFactory;
private final ThreadLocalRequestContext requestContext;
+ private final MessageIdGenerator messageIdGenerator;
private final NotifyResolver.Result notify;
private final ChangeNotes notes;
@@ -81,6 +85,7 @@
private final List<Comment> comments;
private final String patchSetComment;
private final List<LabelVote> labels;
+ private final RepoView repoView;
@Inject
EmailReviewComments(
@@ -88,6 +93,7 @@
PatchSetInfoFactory patchSetInfoFactory,
CommentSender.Factory commentSenderFactory,
ThreadLocalRequestContext requestContext,
+ MessageIdGenerator messageIdGenerator,
@Assisted NotifyResolver.Result notify,
@Assisted ChangeNotes notes,
@Assisted PatchSet patchSet,
@@ -95,11 +101,13 @@
@Assisted ChangeMessage message,
@Assisted List<Comment> comments,
@Nullable @Assisted String patchSetComment,
- @Assisted List<LabelVote> labels) {
+ @Assisted List<LabelVote> labels,
+ @Assisted RepoView repoView) {
this.sendEmailsExecutor = executor;
this.patchSetInfoFactory = patchSetInfoFactory;
this.commentSenderFactory = commentSenderFactory;
this.requestContext = requestContext;
+ this.messageIdGenerator = messageIdGenerator;
this.notify = notify;
this.notes = notes;
this.patchSet = patchSet;
@@ -108,6 +116,7 @@
this.comments = COMMENT_ORDER.sortedCopy(comments);
this.patchSetComment = patchSetComment;
this.labels = labels;
+ this.repoView = repoView;
}
public void sendAsync() {
@@ -127,6 +136,7 @@
cm.setPatchSetComment(patchSetComment);
cm.setLabels(labels);
cm.setNotify(notify);
+ cm.setMessageId(messageIdGenerator.fromChangeUpdate(repoView, patchSet.id()));
cm.send();
} catch (Exception e) {
logger.atSevere().withCause(e).log("Cannot email comments for %s", patchSet.id());
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index 988d178..d4d74a3 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -39,6 +39,7 @@
import com.google.gerrit.server.extensions.events.WorkInProgressStateChanged;
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -79,6 +80,7 @@
private final ChangeMessagesUtil cmUtil;
private final PatchSetUtil psUtil;
private final WorkInProgressStateChanged wipStateChanged;
+ private final MessageIdGenerator messageIdGenerator;
// Assisted-injected fields.
private final PatchSet.Id psId;
@@ -120,6 +122,7 @@
RevisionCreated revisionCreated,
ProjectCache projectCache,
WorkInProgressStateChanged wipStateChanged,
+ MessageIdGenerator messageIdGenerator,
@Assisted ChangeNotes notes,
@Assisted PatchSet.Id psId,
@Assisted ObjectId commitId) {
@@ -133,6 +136,7 @@
this.revisionCreated = revisionCreated;
this.projectCache = projectCache;
this.wipStateChanged = wipStateChanged;
+ this.messageIdGenerator = messageIdGenerator;
this.origNotes = notes;
this.psId = psId;
@@ -291,6 +295,7 @@
cm.addReviewers(oldReviewers.byState(REVIEWER));
cm.addExtraCC(oldReviewers.byState(CC));
cm.setNotify(notify);
+ cm.setMessageId(messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSet.id()));
cm.send();
} catch (Exception err) {
logger.atSevere().withCause(err).log(
diff --git a/java/com/google/gerrit/server/change/SetAssigneeOp.java b/java/com/google/gerrit/server/change/SetAssigneeOp.java
index 9848150..74536aa 100644
--- a/java/com/google/gerrit/server/change/SetAssigneeOp.java
+++ b/java/com/google/gerrit/server/change/SetAssigneeOp.java
@@ -24,6 +24,7 @@
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.extensions.events.AssigneeChanged;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.mail.send.SetAssigneeSender;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.plugincontext.PluginSetContext;
@@ -50,6 +51,7 @@
private final SetAssigneeSender.Factory setAssigneeSenderFactory;
private final Provider<IdentifiedUser> user;
private final IdentifiedUser.GenericFactory userFactory;
+ private final MessageIdGenerator messageIdGenerator;
private Change change;
private IdentifiedUser oldAssignee;
@@ -62,6 +64,7 @@
SetAssigneeSender.Factory setAssigneeSenderFactory,
Provider<IdentifiedUser> user,
IdentifiedUser.GenericFactory userFactory,
+ MessageIdGenerator messageIdGenerator,
@Assisted IdentifiedUser newAssignee) {
this.cmUtil = cmUtil;
this.validationListeners = validationListeners;
@@ -69,6 +72,7 @@
this.setAssigneeSenderFactory = setAssigneeSenderFactory;
this.user = user;
this.userFactory = userFactory;
+ this.messageIdGenerator = messageIdGenerator;
this.newAssignee = requireNonNull(newAssignee, "assignee");
}
@@ -122,6 +126,8 @@
setAssigneeSenderFactory.create(
change.getProject(), change.getId(), newAssignee.getAccountId());
cm.setFrom(user.get().getAccountId());
+ cm.setMessageId(
+ messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
cm.send();
} catch (Exception err) {
logger.atSevere().withCause(err).log(
diff --git a/java/com/google/gerrit/server/change/WorkInProgressOp.java b/java/com/google/gerrit/server/change/WorkInProgressOp.java
index 283cff8..f0ebb80 100644
--- a/java/com/google/gerrit/server/change/WorkInProgressOp.java
+++ b/java/com/google/gerrit/server/change/WorkInProgressOp.java
@@ -20,6 +20,7 @@
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.common.InputWithMessage;
import com.google.gerrit.server.ChangeMessagesUtil;
@@ -30,8 +31,10 @@
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.Context;
+import com.google.gerrit.server.update.RepoView;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
/* Set work in progress or ready for review state on a change */
public class WorkInProgressOp implements BatchUpdateOp {
@@ -131,6 +134,13 @@
|| !sendEmail) {
return;
}
+ RepoView repoView;
+ try {
+ repoView = ctx.getRepoView();
+ } catch (IOException ex) {
+ throw new StorageException(
+ String.format("Repository %s not found", ctx.getProject().get()), ex);
+ }
email
.create(
notify,
@@ -140,7 +150,8 @@
cmsg,
ImmutableList.of(),
cmsg.getMessage(),
- ImmutableList.of())
+ ImmutableList.of(),
+ repoView)
.sendAsync();
}
}
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/config/SysExecutorModule.java b/java/com/google/gerrit/server/config/SysExecutorModule.java
index e7f4540..ea45b12 100644
--- a/java/com/google/gerrit/server/config/SysExecutorModule.java
+++ b/java/com/google/gerrit/server/config/SysExecutorModule.java
@@ -14,7 +14,11 @@
package com.google.gerrit.server.config;
+import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService;
+
+import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.server.CacheRefreshExecutor;
import com.google.gerrit.server.FanOutExecutor;
import com.google.gerrit.server.git.WorkQueue;
import com.google.inject.AbstractModule;
@@ -24,7 +28,7 @@
import org.eclipse.jgit.lib.Config;
/**
- * Module providing the {@link ReceiveCommitsExecutor}.
+ * Module providing different executors.
*
* <p>This module is intended to be installed at the top level when creating a {@code sysInjector}
* in {@code Daemon} or similar, not nested in another module. This ensures the module can be
@@ -37,7 +41,7 @@
@Provides
@Singleton
@ReceiveCommitsExecutor
- public ExecutorService createReceiveCommitsExecutor(
+ public ExecutorService provideReceiveCommitsExecutor(
@GerritServerConfig Config config, WorkQueue queues) {
int poolSize =
config.getInt(
@@ -48,11 +52,11 @@
@Provides
@Singleton
@SendEmailExecutor
- public ExecutorService createSendEmailExecutor(
+ public ExecutorService provideSendEmailExecutor(
@GerritServerConfig Config config, WorkQueue queues) {
int poolSize = config.getInt("sendemail", null, "threadPoolSize", 1);
if (poolSize == 0) {
- return MoreExecutors.newDirectExecutorService();
+ return newDirectExecutorService();
}
return queues.createQueue(poolSize, "SendEmail", true);
}
@@ -60,11 +64,24 @@
@Provides
@Singleton
@FanOutExecutor
- public ExecutorService createFanOutExecutor(@GerritServerConfig Config config, WorkQueue queues) {
+ public ExecutorService provideFanOutExecutor(
+ @GerritServerConfig Config config, WorkQueue queues) {
int poolSize = config.getInt("execution", null, "fanOutThreadPoolSize", 25);
if (poolSize == 0) {
- return MoreExecutors.newDirectExecutorService();
+ return newDirectExecutorService();
}
return queues.createQueue(poolSize, "FanOut");
}
+
+ @Provides
+ @Singleton
+ @CacheRefreshExecutor
+ public ListeningExecutorService provideCacheRefreshExecutor(
+ @GerritServerConfig Config config, WorkQueue queues) {
+ int poolSize = config.getInt("cache", null, "refreshThreadPoolSize", 2);
+ if (poolSize == 0) {
+ return newDirectExecutorService();
+ }
+ return MoreExecutors.listeningDecorator(queues.createQueue(poolSize, "CacheRefresh"));
+ }
}
diff --git a/java/com/google/gerrit/server/config/UrlFormatter.java b/java/com/google/gerrit/server/config/UrlFormatter.java
index 8fc0d9e..d3f90e5 100644
--- a/java/com/google/gerrit/server/config/UrlFormatter.java
+++ b/java/com/google/gerrit/server/config/UrlFormatter.java
@@ -52,6 +52,11 @@
return getChangeViewUrl(change.getProject(), change.getId()).map(url -> url + "?tab=comments");
}
+ /** Returns the URL for viewing the findings tab view of a change. */
+ default Optional<String> getFindingsTabView(Change change) {
+ return getChangeViewUrl(change.getProject(), change.getId()).map(url -> url + "?tab=findings");
+ }
+
/** Returns the URL for viewing a file in a given patch set of a change. */
default Optional<String> getPatchFileView(Change change, int patchsetId, String filename) {
return getChangeViewUrl(change.getProject(), change.getId())
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index df53133..329530c 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -40,6 +40,7 @@
import com.google.gerrit.server.change.ChangeMessages;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.extensions.events.ChangeReverted;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.mail.send.RevertedSender;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
@@ -86,6 +87,7 @@
private final ChangeMessagesUtil cmUtil;
private final ChangeReverted changeReverted;
private final BatchUpdate.Factory updateFactory;
+ private final MessageIdGenerator messageIdGenerator;
@Inject
CommitUtil(
@@ -98,7 +100,8 @@
RevertedSender.Factory revertedSenderFactory,
ChangeMessagesUtil cmUtil,
ChangeReverted changeReverted,
- BatchUpdate.Factory updateFactory) {
+ BatchUpdate.Factory updateFactory,
+ MessageIdGenerator messageIdGenerator) {
this.repoManager = repoManager;
this.serverIdent = serverIdent;
this.seq = seq;
@@ -109,6 +112,7 @@
this.cmUtil = cmUtil;
this.changeReverted = changeReverted;
this.updateFactory = updateFactory;
+ this.messageIdGenerator = messageIdGenerator;
}
public static CommitInfo toCommitInfo(RevCommit commit) throws IOException {
@@ -308,6 +312,8 @@
RevertedSender cm = revertedSenderFactory.create(ctx.getProject(), change.getId());
cm.setFrom(ctx.getAccountId());
cm.setNotify(ctx.getNotify(change.getId()));
+ cm.setMessageId(
+ messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
cm.send();
} catch (Exception err) {
logger.atSevere().withCause(err).log(
diff --git a/java/com/google/gerrit/server/git/MergedByPushOp.java b/java/com/google/gerrit/server/git/MergedByPushOp.java
index b272cba..89d6bd3 100644
--- a/java/com/google/gerrit/server/git/MergedByPushOp.java
+++ b/java/com/google/gerrit/server/git/MergedByPushOp.java
@@ -28,6 +28,7 @@
import com.google.gerrit.server.config.SendEmailExecutor;
import com.google.gerrit.server.extensions.events.ChangeMerged;
import com.google.gerrit.server.mail.send.MergedSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.update.BatchUpdateOp;
@@ -70,6 +71,7 @@
private final PatchSetUtil psUtil;
private final ExecutorService sendEmailExecutor;
private final ChangeMerged changeMerged;
+ private final MessageIdGenerator messageIdGenerator;
private final PatchSet.Id psId;
private final SubmissionId submissionId;
@@ -90,6 +92,7 @@
PatchSetUtil psUtil,
@SendEmailExecutor ExecutorService sendEmailExecutor,
ChangeMerged changeMerged,
+ MessageIdGenerator messageIdGenerator,
@Assisted RequestScopePropagator requestScopePropagator,
@Assisted PatchSet.Id psId,
@Assisted SubmissionId submissionId,
@@ -101,6 +104,7 @@
this.psUtil = psUtil;
this.sendEmailExecutor = sendEmailExecutor;
this.changeMerged = changeMerged;
+ this.messageIdGenerator = messageIdGenerator;
this.requestScopePropagator = requestScopePropagator;
this.submissionId = submissionId;
this.psId = psId;
@@ -189,6 +193,8 @@
mergedSenderFactory.create(ctx.getProject(), psId.changeId());
cm.setFrom(ctx.getAccountId());
cm.setPatchSet(patchSet, info);
+ cm.setMessageId(
+ messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSet.id()));
cm.send();
} catch (Exception e) {
logger.atSevere().withCause(e).log(
diff --git a/java/com/google/gerrit/server/git/WorkQueue.java b/java/com/google/gerrit/server/git/WorkQueue.java
index f2a0ff1..4b08040 100644
--- a/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/java/com/google/gerrit/server/git/WorkQueue.java
@@ -606,9 +606,12 @@
@Override
public void run() {
if (running.compareAndSet(false, true)) {
+ String oldThreadName = Thread.currentThread().getName();
try {
+ Thread.currentThread().setName(oldThreadName + "[" + task.toString() + "]");
task.run();
} finally {
+ Thread.currentThread().setName(oldThreadName);
if (isPeriodic()) {
running.set(false);
} else {
@@ -681,5 +684,10 @@
public boolean hasCustomizedPrint() {
return runnable.hasCustomizedPrint();
}
+
+ @Override
+ public String toString() {
+ return runnable.toString();
+ }
}
}
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index 0baecf5..f5c69e0 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -61,6 +61,7 @@
import com.google.gerrit.server.git.MergedByPushOp;
import com.google.gerrit.server.git.receive.ReceiveCommits.MagicBranchInput;
import com.google.gerrit.server.mail.MailUtil.MailRecipients;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -128,6 +129,8 @@
private final ReplacePatchSetSender.Factory replacePatchSetFactory;
private final ProjectCache projectCache;
private final ReviewerAdder reviewerAdder;
+ private final Change change;
+ private final MessageIdGenerator messageIdGenerator;
private final ProjectState projectState;
private final BranchNameKey dest;
@@ -140,7 +143,6 @@
private final PatchSetInfo info;
private final MagicBranchInput magicBranch;
private final PushCertificate pushCertificate;
- private final Change change;
private List<String> groups;
private final Map<String, Short> approvals = new HashMap<>();
@@ -172,6 +174,7 @@
@SendEmailExecutor ExecutorService sendEmailExecutor,
ReviewerAdder reviewerAdder,
Change change,
+ MessageIdGenerator messageIdGenerator,
@Assisted ProjectState projectState,
@Assisted BranchNameKey dest,
@Assisted boolean checkMergedInto,
@@ -197,6 +200,8 @@
this.projectCache = projectCache;
this.sendEmailExecutor = sendEmailExecutor;
this.reviewerAdder = reviewerAdder;
+ this.change = change;
+ this.messageIdGenerator = messageIdGenerator;
this.projectState = projectState;
this.dest = dest;
@@ -210,7 +215,6 @@
this.groups = groups;
this.magicBranch = magicBranch;
this.pushCertificate = pushCertificate;
- this.change = change;
}
@Override
@@ -533,6 +537,7 @@
oldRecipients.getCcOnly().stream(),
reviewerAdditions.flattenResults(AddReviewersOp.Result::addedCCs).stream())
.collect(toImmutableSet()));
+ cm.setMessageId(messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), patchSetId));
// TODO(dborowitz): Support byEmail
cm.send();
} catch (Exception e) {
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 3bb4770..1a4a335 100644
--- a/java/com/google/gerrit/server/logging/Metadata.java
+++ b/java/com/google/gerrit/server/logging/Metadata.java
@@ -31,116 +31,126 @@
/** Metadata that is provided to {@link PerformanceLogger}s as context for performance records. */
@AutoValue
public abstract class Metadata {
- // The numeric ID of an account.
+ /** The numeric ID of an account. */
public abstract Optional<Integer> accountId();
- // The type of an action (ACCOUNT_UPDATE, CHANGE_UPDATE, GROUP_UPDATE, INDEX_QUERY,
- // PLUGIN_UPDATE).
+ /**
+ * The type of an action (ACCOUNT_UPDATE, CHANGE_UPDATE, GROUP_UPDATE, INDEX_QUERY,
+ * PLUGIN_UPDATE).
+ */
public abstract Optional<String> actionType();
- // An authentication domain name.
+ /** An authentication domain name. */
public abstract Optional<String> authDomainName();
- // The name of a branch.
+ /** The name of a branch. */
public abstract Optional<String> branchName();
- // Key of an entity in a cache.
+ /** Key of an entity in a cache. */
public abstract Optional<String> cacheKey();
- // The name of a cache.
+ /** The name of a cache. */
public abstract Optional<String> cacheName();
- // The name of the implementation class.
+ /** The name of the implementation class. */
public abstract Optional<String> className();
- // The numeric ID of a change.
+ /** The numeric ID of a change. */
public abstract Optional<Integer> changeId();
- // The type of change ID which the user used to identify a change (e.g. numeric ID, triplet etc.).
+ /**
+ * The type of change ID which the user used to identify a change (e.g. numeric ID, triplet etc.).
+ */
public abstract Optional<String> changeIdType();
- // The cause of an error.
+ /** The cause of an error. */
public abstract Optional<String> cause();
- // The type of an event.
+ /** The SHA1 of a commit. */
+ public abstract Optional<String> commit();
+
+ /** The type of an event. */
public abstract Optional<String> eventType();
- // The value of the @Export annotation which was used to register a plugin extension.
+ /** The value of the @Export annotation which was used to register a plugin extension. */
public abstract Optional<String> exportValue();
- // Path of a file in a repository.
+ /** Path of a file in a repository. */
public abstract Optional<String> filePath();
- // Garbage collector name.
+ /** Garbage collector name. */
public abstract Optional<String> garbageCollectorName();
- // Git operation (CLONE, FETCH).
+ /** Git operation (CLONE, FETCH). */
public abstract Optional<String> gitOperation();
- // The numeric ID of an internal group.
+ /** The numeric ID of an internal group. */
public abstract Optional<Integer> groupId();
- // The name of a group.
+ /** The name of a group. */
public abstract Optional<String> groupName();
- // The UUID of a group.
+ /** The UUID of a group. */
public abstract Optional<String> groupUuid();
- // HTTP status response code.
+ /** HTTP status response code. */
public abstract Optional<Integer> httpStatus();
- // The name of a secondary index.
+ /** The name of a secondary index. */
public abstract Optional<String> indexName();
- // The version of a secondary index.
+ /** The version of a secondary index. */
public abstract Optional<Integer> indexVersion();
- // The name of the implementation method.
+ /** The name of the implementation method. */
public abstract Optional<String> methodName();
- // One or more resources
+ /** One or more resources */
public abstract Optional<Boolean> multiple();
- // The name of an operation that is performed.
+ /** The name of an operation that is performed. */
public abstract Optional<String> operationName();
- // Partial or full computation
+ /** Partial or full computation */
public abstract Optional<Boolean> partial();
- // Path of a metadata file in NoteDb.
+ /** If a value is still current or not */
+ public abstract Optional<Boolean> outdated();
+
+ /** Path of a metadata file in NoteDb. */
public abstract Optional<String> noteDbFilePath();
- // Name of a metadata ref in NoteDb.
+ /** Name of a metadata ref in NoteDb. */
public abstract Optional<String> noteDbRefName();
- // Type of a sequence in NoteDb (ACCOUNTS, CHANGES, GROUPS).
+ /** Type of a sequence in NoteDb (ACCOUNTS, CHANGES, GROUPS). */
public abstract Optional<String> noteDbSequenceType();
- // The ID of a patch set.
+ /** The ID of a patch set. */
public abstract Optional<Integer> patchSetId();
- // Plugin metadata that doesn't fit into any other category.
+ /** Plugin metadata that doesn't fit into any other category. */
public abstract ImmutableList<PluginMetadata> pluginMetadata();
- // The name of a plugin.
+ /** The name of a plugin. */
public abstract Optional<String> pluginName();
- // The name of a Gerrit project (aka Git repository).
+ /** The name of a Gerrit project (aka Git repository). */
public abstract Optional<String> projectName();
- // The type of a Git push to Gerrit (CREATE_REPLACE, NORMAL, AUTOCLOSE).
+ /** The type of a Git push to Gerrit (CREATE_REPLACE, NORMAL, AUTOCLOSE). */
public abstract Optional<String> pushType();
- // The number of resources that is processed.
+ /** The number of resources that is processed. */
public abstract Optional<Integer> resourceCount();
- // The name of a REST view.
+ /** The name of a REST view. */
public abstract Optional<String> restViewName();
- // The SHA1 of Git commit.
+ /** The SHA1 of Git commit. */
public abstract Optional<String> revision();
- // The username of an account.
+ /** The username of an account. */
public abstract Optional<String> username();
/**
@@ -275,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);
@@ -305,6 +317,8 @@
public abstract Builder partial(boolean partial);
+ public abstract Builder outdated(boolean outdated);
+
public abstract Builder noteDbFilePath(@Nullable String noteDbFilePath);
public abstract Builder noteDbRefName(@Nullable String noteDbRefName);
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index 9c3dd02..f004e4b 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -57,6 +57,7 @@
import com.google.gerrit.server.extensions.events.CommentAdded;
import com.google.gerrit.server.mail.MailFilter;
import com.google.gerrit.server.mail.send.InboundEmailRejectionSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
@@ -115,6 +116,7 @@
private final AccountCache accountCache;
private final DynamicItem<UrlFormatter> urlFormatter;
private final PluginSetContext<CommentValidator> commentValidators;
+ private final MessageIdGenerator messageIdGenerator;
@Inject
public MailProcessor(
@@ -133,7 +135,8 @@
CommentAdded commentAdded,
AccountCache accountCache,
DynamicItem<UrlFormatter> urlFormatter,
- PluginSetContext<CommentValidator> commentValidators) {
+ PluginSetContext<CommentValidator> commentValidators,
+ MessageIdGenerator messageIdGenerator) {
this.emails = emails;
this.emailRejectionSender = emailRejectionSender;
this.retryHelper = retryHelper;
@@ -150,6 +153,7 @@
this.accountCache = accountCache;
this.urlFormatter = urlFormatter;
this.commentValidators = commentValidators;
+ this.messageIdGenerator = messageIdGenerator;
}
/**
@@ -222,6 +226,7 @@
try {
InboundEmailRejectionSender em =
emailRejectionSender.create(message.from(), message.id(), reason);
+ em.setMessageId(messageIdGenerator.fromMailMessage(message));
em.send();
} catch (Exception e) {
logger.atSevere().withCause(e).log("Cannot send email to warn for an error");
@@ -366,7 +371,8 @@
changeMessage,
comments,
patchSetComment,
- ImmutableList.of())
+ ImmutableList.of(),
+ ctx.getRepoView())
.sendAsync();
// Get previous approvals from this user
Map<String, Short> approvals = new HashMap<>();
diff --git a/java/com/google/gerrit/server/mail/send/AddKeySender.java b/java/com/google/gerrit/server/mail/send/AddKeySender.java
index 3b7b2aa..e02f02a 100644
--- a/java/com/google/gerrit/server/mail/send/AddKeySender.java
+++ b/java/com/google/gerrit/server/mail/send/AddKeySender.java
@@ -35,11 +35,16 @@
private final IdentifiedUser user;
private final AccountSshKey sshKey;
private final List<String> gpgKeys;
+ private final MessageIdGenerator messageIdGenerator;
@AssistedInject
public AddKeySender(
- EmailArguments args, @Assisted IdentifiedUser user, @Assisted AccountSshKey sshKey) {
+ EmailArguments args,
+ MessageIdGenerator messageIdGenerator,
+ @Assisted IdentifiedUser user,
+ @Assisted AccountSshKey sshKey) {
super(args, "addkey");
+ this.messageIdGenerator = messageIdGenerator;
this.user = user;
this.sshKey = sshKey;
this.gpgKeys = null;
@@ -47,8 +52,12 @@
@AssistedInject
public AddKeySender(
- EmailArguments args, @Assisted IdentifiedUser user, @Assisted List<String> gpgKeys) {
+ EmailArguments args,
+ MessageIdGenerator messageIdGenerator,
+ @Assisted IdentifiedUser user,
+ @Assisted List<String> gpgKeys) {
super(args, "addkey");
+ this.messageIdGenerator = messageIdGenerator;
this.user = user;
this.sshKey = null;
this.gpgKeys = gpgKeys;
@@ -58,6 +67,7 @@
protected void init() throws EmailException {
super.init();
setHeader("Subject", String.format("[Gerrit Code Review] New %s Keys Added", getKeyType()));
+ setMessageId(messageIdGenerator.fromAccountUpdate(user.getAccountId()));
add(RecipientType.TO, Address.create(getEmail()));
}
diff --git a/java/com/google/gerrit/server/mail/send/CommentSender.java b/java/com/google/gerrit/server/mail/send/CommentSender.java
index deaa926..f3cccf2 100644
--- a/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -93,6 +93,11 @@
return args.urlFormatter.get().getCommentsTabView(change).orElse(null);
}
+ /** @return a web link to the findings tab view of a change. */
+ public String getFindingsTabLink() {
+ return args.urlFormatter.get().getFindingsTabView(change).orElse(null);
+ }
+
/**
* @return A title for the group, i.e. "Commit Message", "Merge List", or "File [[filename]]".
*/
@@ -386,9 +391,7 @@
for (CommentSender.FileCommentGroup group : getGroupedInlineComments(repo)) {
Map<String, Object> groupData = new HashMap<>();
- if (group.filename.equals(Patch.PATCHSET_LEVEL)) {
- groupData.put("link", group.getCommentsTabLink());
- } else {
+ if (!group.filename.equals(Patch.PATCHSET_LEVEL)) {
groupData.put("link", group.getFileLink());
}
groupData.put("title", group.getTitle());
@@ -420,7 +423,11 @@
// Set the comment link.
if (comment.key.filename.equals(Patch.PATCHSET_LEVEL)) {
- commentData.put("link", group.getCommentsTabLink());
+ if (comment instanceof RobotComment) {
+ commentData.put("link", group.getFindingsTabLink());
+ } else {
+ commentData.put("link", group.getCommentsTabLink());
+ }
} else if (comment.lineNbr == 0) {
commentData.put("link", group.getFileLink());
} else {
diff --git a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
index 3df7f05..ce336ff 100644
--- a/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
+++ b/java/com/google/gerrit/server/mail/send/DeleteKeySender.java
@@ -38,11 +38,16 @@
private final IdentifiedUser user;
private final AccountSshKey sshKey;
private final List<String> gpgKeyFingerprints;
+ private final MessageIdGenerator messageIdGenerator;
@AssistedInject
public DeleteKeySender(
- EmailArguments args, @Assisted IdentifiedUser user, @Assisted AccountSshKey sshKey) {
+ EmailArguments args,
+ MessageIdGenerator messageIdGenerator,
+ @Assisted IdentifiedUser user,
+ @Assisted AccountSshKey sshKey) {
super(args, "deletekey");
+ this.messageIdGenerator = messageIdGenerator;
this.user = user;
this.gpgKeyFingerprints = Collections.emptyList();
this.sshKey = sshKey;
@@ -51,9 +56,11 @@
@AssistedInject
public DeleteKeySender(
EmailArguments args,
+ MessageIdGenerator messageIdGenerator,
@Assisted IdentifiedUser user,
@Assisted List<String> gpgKeyFingerprints) {
super(args, "deletekey");
+ this.messageIdGenerator = messageIdGenerator;
this.user = user;
this.gpgKeyFingerprints = gpgKeyFingerprints;
this.sshKey = null;
@@ -63,6 +70,7 @@
protected void init() throws EmailException {
super.init();
setHeader("Subject", String.format("[Gerrit Code Review] %s Keys Deleted", getKeyType()));
+ setMessageId(messageIdGenerator.fromAccountUpdate(user.getAccountId()));
add(RecipientType.TO, Address.create(getEmail()));
}
diff --git a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
index cec2bb5..bca5338 100644
--- a/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
+++ b/java/com/google/gerrit/server/mail/send/HttpPasswordUpdateSender.java
@@ -18,6 +18,7 @@
import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.mail.Address;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
@@ -29,11 +30,16 @@
private final IdentifiedUser user;
private final String operation;
+ private final MessageIdGenerator messageIdGenerator;
@AssistedInject
public HttpPasswordUpdateSender(
- EmailArguments args, @Assisted IdentifiedUser user, @Assisted String operation) {
+ EmailArguments args,
+ MessageIdGenerator messageIdGenerator,
+ @Assisted IdentifiedUser user,
+ @Assisted String operation) {
super(args, "HttpPasswordUpdate");
+ this.messageIdGenerator = messageIdGenerator;
this.user = user;
this.operation = operation;
}
@@ -42,6 +48,9 @@
protected void init() throws EmailException {
super.init();
setHeader("Subject", "[Gerrit Code Review] HTTP password was " + operation);
+ setMessageId(
+ messageIdGenerator.fromReasonAccountIdAndTimestamp(
+ "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
new file mode 100644
index 0000000..3a411dc
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/send/MessageIdGenerator.java
@@ -0,0 +1,126 @@
+// 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.mail.send;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.mail.MailMessage;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.update.RepoView;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+/** A generator class that creates a {@link MessageId} */
+public class MessageIdGenerator {
+ private final GitRepositoryManager repositoryManager;
+ private final AllUsersName allUsersName;
+
+ @Inject
+ public MessageIdGenerator(GitRepositoryManager repositoryManager, AllUsersName allUsersName) {
+ this.repositoryManager = repositoryManager;
+ this.allUsersName = allUsersName;
+ }
+
+ /**
+ * A unique id used which is a part of the header of all emails sent through by Gerrit. All of the
+ * emails are sent via {@link OutgoingEmail#send()}.
+ */
+ @AutoValue
+ public abstract static class MessageId {
+ public abstract String id();
+ }
+
+ /**
+ * Create a {@link MessageId} as a result of a change update.
+ *
+ * @param repoView
+ * @param patchsetId
+ * @return MessageId that depends on the patchset.
+ */
+ public MessageId fromChangeUpdate(RepoView repoView, PatchSet.Id patchsetId) {
+ String metaRef = patchsetId.changeId().toRefPrefix() + "meta";
+ Optional<ObjectId> metaSha1;
+ try {
+ metaSha1 = repoView.getRef(metaRef);
+ } catch (IOException ex) {
+ throw new StorageException("unable to extract info for Message-Id", ex);
+ }
+ return metaSha1
+ .map(optional -> new AutoValue_MessageIdGenerator_MessageId(optional.getName()))
+ .orElseThrow(() -> new IllegalStateException(metaRef + " doesn't exist"));
+ }
+
+ public MessageId fromChangeUpdate(Project.NameKey project, PatchSet.Id patchsetId) {
+ String metaRef = patchsetId.changeId().toRefPrefix() + "meta";
+ Ref ref = getRef(metaRef, project);
+ checkState(ref != null, metaRef + " must exist");
+ return new AutoValue_MessageIdGenerator_MessageId(ref.getObjectId().getName());
+ }
+
+ /**
+ * @param accountId Create a {@link MessageId} as a result of an account update.
+ * @return MessageId that depends on the account id.
+ */
+ public MessageId fromAccountUpdate(Account.Id accountId) {
+ String userRef = RefNames.refsUsers(accountId);
+ Ref ref = getRef(userRef, allUsersName);
+ checkState(ref != null, userRef + " must exist");
+ return new AutoValue_MessageIdGenerator_MessageId(ref.getObjectId().getName());
+ }
+
+ /**
+ * Create a {@link MessageId} from a mail message.
+ *
+ * @param mailMessage The message that was sent but was rejected.
+ * @return MessageId that depends on the MailMessage that was rejected.
+ */
+ public MessageId fromMailMessage(MailMessage mailMessage) {
+ return new AutoValue_MessageIdGenerator_MessageId(mailMessage.id() + "-REJECTION");
+ }
+
+ /**
+ * Create a {@link MessageId} from a reason, Account.Id, and timestamp.
+ *
+ * @param reason for performing this account update
+ * @param accountId
+ * @param timestamp
+ * @return MessageId that depends on the reason, accountId, and timestamp.
+ */
+ public MessageId fromReasonAccountIdAndTimestamp(
+ String reason, Account.Id accountId, Instant timestamp) {
+ return new AutoValue_MessageIdGenerator_MessageId(
+ reason + "-" + accountId.toString() + "-" + timestamp.toString());
+ }
+
+ private Ref getRef(String userRef, Project.NameKey project) {
+ try (Repository repository = repositoryManager.openRepository(project)) {
+ return repository.getRefDatabase().findRef(userRef);
+ } catch (IOException ex) {
+ throw new StorageException("unable to extract info for Message-Id", ex);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/mail/send/NewChangeSender.java b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
index 83c3a94..d81dca4 100644
--- a/java/com/google/gerrit/server/mail/send/NewChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
@@ -57,7 +57,6 @@
super.init();
String threadId = getChangeMessageThreadId();
- setHeader("Message-ID", threadId);
setHeader("References", threadId);
switch (notify.handling()) {
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index b35bbec..8f63177 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -67,6 +67,7 @@
private Address smtpFromAddress;
private StringBuilder textBody;
private StringBuilder htmlBody;
+ private MessageIdGenerator.MessageId messageId;
protected Map<String, Object> soyContext;
protected Map<String, Object> soyContextEmailData;
protected List<String> footers;
@@ -88,6 +89,10 @@
this.notify = requireNonNull(notify);
}
+ public void setMessageId(MessageIdGenerator.MessageId messageId) {
+ this.messageId = messageId;
+ }
+
/**
* Format and enqueue the message for delivery.
*
@@ -108,6 +113,9 @@
}
init();
+ if (messageId == null) {
+ throw new IllegalStateException("All emails must have a messageId");
+ }
if (useHtml()) {
appendHtml(soyHtmlTemplate("HeaderHtml"));
}
@@ -201,31 +209,21 @@
va.htmlBody = null;
}
- for (OutgoingEmailValidationListener validator : args.outgoingEmailValidationListeners) {
- try {
- validator.validateOutgoingEmail(va);
- } catch (ValidationException e) {
- logger.atFine().log(
- "Not sending '%s': Rejected by outgoing email validator: %s",
- messageClass, e.getMessage());
- return;
- }
- }
-
Set<Address> intersection = Sets.intersection(va.smtpRcptTo, smtpRcptToPlaintextOnly);
if (!intersection.isEmpty()) {
logger.atSevere().log("Email '%s' will be sent twice to %s", messageClass, intersection);
}
-
if (!va.smtpRcptTo.isEmpty()) {
// Send multipart message
+ addMessageId(va, "-HTML");
+ if (!validateEmail(va)) return;
logger.atFine().log(
"Sending multipart '%s' from %s to %s",
messageClass, va.smtpFromAddress, va.smtpRcptTo);
args.emailSender.send(va.smtpFromAddress, va.smtpRcptTo, va.headers, va.body, va.htmlBody);
}
-
if (!smtpRcptToPlaintextOnly.isEmpty()) {
+ addMessageId(va, "-PLAIN");
// Send plaintext message
Map<String, EmailHeader> shallowCopy = new HashMap<>();
shallowCopy.putAll(headers);
@@ -238,6 +236,7 @@
to.add(a);
shallowCopy.put(FieldName.TO, to);
}
+ if (!validateEmail(va)) return;
logger.atFine().log(
"Sending plaintext '%s' from %s to %s",
messageClass, va.smtpFromAddress, smtpRcptToPlaintextOnly);
@@ -246,6 +245,29 @@
}
}
+ private boolean validateEmail(OutgoingEmailValidationListener.Args va) {
+ for (OutgoingEmailValidationListener validator : args.outgoingEmailValidationListeners) {
+ try {
+ validator.validateOutgoingEmail(va);
+ } catch (ValidationException e) {
+ logger.atFine().log(
+ "Not sending '%s': Rejected by outgoing email validator: %s",
+ messageClass, e.getMessage());
+ return false;
+ }
+ }
+ 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) {
+ String message = "<" + messageId.id() + suffix + "@" + getGerritHost() + ">";
+ message = message.replaceAll("\\s", "");
+ va.headers.put(FieldName.MESSAGE_ID, new EmailHeader.String(message));
+ }
+ }
+
/** Format the message body by calling {@link #appendText(String)}. */
protected abstract void format() throws EmailException;
@@ -262,7 +284,6 @@
headers.put(FieldName.FROM, new EmailHeader.AddressList(smtpFromAddress));
headers.put(FieldName.TO, new EmailHeader.AddressList());
headers.put(FieldName.CC, new EmailHeader.AddressList());
- setHeader(FieldName.MESSAGE_ID, "");
setHeader(MailHeader.AUTO_SUBMITTED.fieldName(), "auto-generated");
for (RecipientType recipientType : notify.accounts().keySet()) {
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/DeleteZombieCommentsRefs.java b/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
index 128e185..1ead03c 100644
--- a/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
+++ b/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
@@ -27,6 +27,7 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
+import java.util.function.Consumer;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
@@ -47,12 +48,16 @@
private static final String EMPTY_TREE_ID = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
private static final String DRAFT_REFS_PREFIX = "refs/draft-comments";
- private static final int CHUNK_SIZE = 100; // log progress after deleting every CHUNK_SIZE refs
+
+ // Number of refs deleted at once in a batch ref-update.
+ // Log progress after deleting every CHUNK_SIZE refs
+ private static final int CHUNK_SIZE = 3000;
private final GitRepositoryManager repoManager;
private final AllUsersName allUsers;
private final int cleanupPercentage;
private Repository allUsersRepo;
+ private final Consumer<String> uiConsumer;
public interface Factory {
DeleteZombieCommentsRefs create(int cleanupPercentage);
@@ -63,9 +68,18 @@
AllUsersName allUsers,
GitRepositoryManager repoManager,
@Assisted Integer cleanupPercentage) {
+ this(allUsers, repoManager, cleanupPercentage, (msg) -> {});
+ }
+
+ public DeleteZombieCommentsRefs(
+ AllUsersName allUsers,
+ GitRepositoryManager repoManager,
+ Integer cleanupPercentage,
+ Consumer<String> uiConsumer) {
this.allUsers = allUsers;
this.repoManager = repoManager;
this.cleanupPercentage = (cleanupPercentage == null) ? 100 : cleanupPercentage;
+ this.uiConsumer = uiConsumer;
}
public void execute() throws IOException {
@@ -74,15 +88,17 @@
List<Ref> draftRefs = allUsersRepo.getRefDatabase().getRefsByPrefix(DRAFT_REFS_PREFIX);
List<Ref> zombieRefs = filterZombieRefs(draftRefs);
- logger.atInfo().log(
- "Found a total of %d zombie draft refs in %s repo.", zombieRefs.size(), allUsers.get());
+ logInfo(
+ String.format(
+ "Found a total of %d zombie draft refs in %s repo.",
+ zombieRefs.size(), allUsers.get()));
- logger.atInfo().log("Cleanup percentage = %d", cleanupPercentage);
+ logInfo(String.format("Cleanup percentage = %d", cleanupPercentage));
zombieRefs =
zombieRefs.stream()
.filter(ref -> Change.Id.fromAllUsersRef(ref.getName()).get() % 100 < cleanupPercentage)
.collect(toImmutableList());
- logger.atInfo().log("Number of zombie refs to be cleaned = %d", zombieRefs.size());
+ logInfo(String.format("Number of zombie refs to be cleaned = %d", zombieRefs.size()));
long zombieRefsCnt = zombieRefs.size();
long deletedRefsCnt = 0;
@@ -124,8 +140,15 @@
return allUsersRepo.parseCommit(ref.getObjectId()).getTree().getName().equals(EMPTY_TREE_ID);
}
+ private void logInfo(String message) {
+ logger.atInfo().log(message);
+ uiConsumer.accept(message);
+ }
+
private void logProgress(long deletedRefsCount, long allRefsCount, long elapsed) {
- logger.atInfo().log(
- "Deleted %d/%d zombie draft refs (%d seconds)\n", deletedRefsCount, allRefsCount, elapsed);
+ logInfo(
+ String.format(
+ "Deleted %d/%d zombie draft refs (%d seconds)",
+ deletedRefsCount, allRefsCount, elapsed));
}
}
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/patch/DiffExecutorModule.java b/java/com/google/gerrit/server/patch/DiffExecutorModule.java
index eb6a280..b9e644f 100644
--- a/java/com/google/gerrit/server/patch/DiffExecutorModule.java
+++ b/java/com/google/gerrit/server/patch/DiffExecutorModule.java
@@ -31,7 +31,7 @@
@Provides
@Singleton
@DiffExecutor
- public ExecutorService createDiffExecutor() {
+ public ExecutorService provideDiffExecutor() {
return new LoggingContextAwareExecutorService(
Executors.newCachedThreadPool(
new ThreadFactoryBuilder().setNameFormat("Diff-%d").setDaemon(true).build()));
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/plugins/JarPluginProvider.java b/java/com/google/gerrit/server/plugins/JarPluginProvider.java
index 5b80059..82f97c9 100644
--- a/java/com/google/gerrit/server/plugins/JarPluginProvider.java
+++ b/java/com/google/gerrit/server/plugins/JarPluginProvider.java
@@ -110,9 +110,6 @@
public static Path storeInTemp(String pluginName, InputStream in, SitePaths sitePaths)
throws IOException {
- if (!Files.exists(sitePaths.tmp_dir)) {
- Files.createDirectories(sitePaths.tmp_dir);
- }
return PluginUtil.asTemp(in, tempNameFor(pluginName), ".jar", sitePaths.tmp_dir);
}
diff --git a/java/com/google/gerrit/server/plugins/PluginUtil.java b/java/com/google/gerrit/server/plugins/PluginUtil.java
index d4110ca..932a01d 100644
--- a/java/com/google/gerrit/server/plugins/PluginUtil.java
+++ b/java/com/google/gerrit/server/plugins/PluginUtil.java
@@ -53,6 +53,7 @@
}
static Path asTemp(InputStream in, String prefix, String suffix, Path dir) throws IOException {
+ Files.createDirectories(dir);
Path tmp = Files.createTempFile(dir, prefix, suffix);
boolean keep = false;
try (OutputStream out = Files.newOutputStream(tmp)) {
diff --git a/java/com/google/gerrit/server/project/ProjectCacheClock.java b/java/com/google/gerrit/server/project/ProjectCacheClock.java
deleted file mode 100644
index eb451fd..0000000
--- a/java/com/google/gerrit/server/project/ProjectCacheClock.java
+++ /dev/null
@@ -1,100 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.project;
-
-import com.google.common.util.concurrent.ThreadFactoryBuilder;
-import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.logging.LoggingContextAwareScheduledExecutorService;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicLong;
-import org.eclipse.jgit.lib.Config;
-
-/** Ticks periodically to force refresh events for {@link ProjectCacheImpl}. */
-@Singleton
-public class ProjectCacheClock implements LifecycleListener {
- private final Config serverConfig;
-
- private final AtomicLong generation = new AtomicLong();
-
- private ScheduledExecutorService executor;
-
- @Inject
- public ProjectCacheClock(@GerritServerConfig Config serverConfig) {
- this.serverConfig = serverConfig;
- }
-
- @Override
- public void start() {
- long checkFrequencyMillis = checkFrequency(serverConfig);
-
- if (checkFrequencyMillis == Long.MAX_VALUE) {
- // Start with generation 1 (to avoid magic 0 below).
- // Do not begin background thread, disabling the clock.
- generation.set(1);
- } else if (10 < checkFrequencyMillis) {
- // Start with generation 1 (to avoid magic 0 below).
- generation.set(1);
- executor =
- new LoggingContextAwareScheduledExecutorService(
- Executors.newScheduledThreadPool(
- 1,
- new ThreadFactoryBuilder()
- .setNameFormat("ProjectCacheClock-%d")
- .setDaemon(true)
- .setPriority(Thread.MIN_PRIORITY)
- .build()));
- @SuppressWarnings("unused") // Runnable already handles errors
- Future<?> possiblyIgnoredError =
- executor.scheduleAtFixedRate(
- generation::incrementAndGet,
- checkFrequencyMillis,
- checkFrequencyMillis,
- TimeUnit.MILLISECONDS);
- } else {
- // Magic generation 0 triggers ProjectState to always
- // check on each needsRefresh() request we make to it.
- generation.set(0);
- }
- }
-
- @Override
- public void stop() {
- if (executor != null) {
- executor.shutdown();
- }
- }
-
- long read() {
- return generation.get();
- }
-
- private static long checkFrequency(Config serverConfig) {
- String freq = serverConfig.getString("cache", "projects", "checkFrequency");
- if (freq != null && ("disabled".equalsIgnoreCase(freq) || "off".equalsIgnoreCase(freq))) {
- return Long.MAX_VALUE;
- }
- return TimeUnit.MILLISECONDS.convert(
- ConfigUtil.getTimeUnit(
- serverConfig, "cache", "projects", "checkFrequency", 5, TimeUnit.MINUTES),
- TimeUnit.MINUTES);
- }
-}
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 9d09eeb..a5c07e5 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -24,16 +24,23 @@
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.project.ProjectIndexer;
import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.metrics.Counter2;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.Field;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.server.CacheRefreshExecutor;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllUsersName;
@@ -48,6 +55,7 @@
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import java.io.IOException;
+import java.time.Duration;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -55,6 +63,7 @@
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
/** Cache of project information, including access rights. */
@@ -70,7 +79,10 @@
return new CacheModule() {
@Override
protected void configure() {
- cache(CACHE_NAME, String.class, ProjectState.class).loader(Loader.class);
+ cache(CACHE_NAME, Project.NameKey.class, ProjectState.class)
+ .loader(Loader.class)
+ .refreshAfterWrite(Duration.ofMinutes(15))
+ .expireAfterWrite(Duration.ofHours(1));
cache(CACHE_LIST, ListKey.class, new TypeLiteral<ImmutableSortedSet<Project.NameKey>>() {})
.maximumWeight(1)
@@ -84,7 +96,6 @@
@Override
protected void configure() {
listener().to(ProjectCacheWarmer.class);
- listener().to(ProjectCacheClock.class);
}
});
}
@@ -93,10 +104,9 @@
private final AllProjectsName allProjectsName;
private final AllUsersName allUsersName;
- private final LoadingCache<String, ProjectState> byName;
+ private final LoadingCache<Project.NameKey, ProjectState> byName;
private final LoadingCache<ListKey, ImmutableSortedSet<Project.NameKey>> list;
private final Lock listLock;
- private final ProjectCacheClock clock;
private final Provider<ProjectIndexer> indexer;
private final Timer0 guessRelevantGroupsLatency;
@@ -104,9 +114,8 @@
ProjectCacheImpl(
final AllProjectsName allProjectsName,
final AllUsersName allUsersName,
- @Named(CACHE_NAME) LoadingCache<String, ProjectState> byName,
+ @Named(CACHE_NAME) LoadingCache<Project.NameKey, ProjectState> byName,
@Named(CACHE_LIST) LoadingCache<ListKey, ImmutableSortedSet<Project.NameKey>> list,
- ProjectCacheClock clock,
Provider<ProjectIndexer> indexer,
MetricMaker metricMaker) {
this.allProjectsName = allProjectsName;
@@ -114,7 +123,6 @@
this.byName = byName;
this.list = list;
this.listLock = new ReentrantLock(true /* fair */);
- this.clock = clock;
this.indexer = indexer;
this.guessRelevantGroupsLatency =
@@ -142,13 +150,8 @@
}
try {
- ProjectState state = byName.get(projectName.get());
- if (state != null && state.needsRefresh(clock.read())) {
- byName.invalidate(projectName.get());
- state = byName.get(projectName.get());
- }
- return Optional.of(state);
- } catch (Exception e) {
+ return Optional.of(byName.get(projectName));
+ } catch (ExecutionException e) {
if ((e.getCause() instanceof RepositoryNotFoundException)) {
logger.atFine().log("Cannot find project %s", projectName.get());
return Optional.empty();
@@ -167,7 +170,7 @@
public void evict(Project.NameKey p) {
if (p != null) {
logger.atFine().log("Evict project '%s'", p.get());
- byName.invalidate(p.get());
+ byName.invalidate(p);
}
indexer.get().index(p);
}
@@ -222,7 +225,7 @@
public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
try (Timer0.Context ignored = guessRelevantGroupsLatency.start()) {
return all().stream()
- .map(n -> byName.getIfPresent(n.get()))
+ .map(n -> byName.getIfPresent(n))
.filter(Objects::nonNull)
.flatMap(p -> p.getConfig().getAllGroupUUIDs().stream())
// getAllGroupUUIDs shouldn't really return null UUIDs, but harden
@@ -245,41 +248,67 @@
}
}
- static class Loader extends CacheLoader<String, ProjectState> {
+ @Singleton
+ static class Loader extends CacheLoader<Project.NameKey, ProjectState> {
private final ProjectState.Factory projectStateFactory;
private final GitRepositoryManager mgr;
- private final ProjectCacheClock clock;
private final ProjectConfig.Factory projectConfigFactory;
+ private final ListeningExecutorService cacheRefreshExecutor;
+ private final Counter2<String, Boolean> refreshCounter;
@Inject
Loader(
ProjectState.Factory psf,
GitRepositoryManager g,
- ProjectCacheClock clock,
- ProjectConfig.Factory projectConfigFactory) {
+ ProjectConfig.Factory projectConfigFactory,
+ @CacheRefreshExecutor ListeningExecutorService cacheRefreshExecutor,
+ MetricMaker metricMaker) {
projectStateFactory = psf;
mgr = g;
- this.clock = clock;
this.projectConfigFactory = projectConfigFactory;
+ this.cacheRefreshExecutor = cacheRefreshExecutor;
+ refreshCounter =
+ metricMaker.newCounter(
+ "caches/refresh_count",
+ new Description("count").setRate(),
+ Field.ofString("cache", Metadata.Builder::className).build(),
+ Field.ofBoolean("outdated", Metadata.Builder::outdated).build());
}
@Override
- public ProjectState load(String projectName) throws Exception {
+ public ProjectState load(Project.NameKey key) throws Exception {
try (TraceTimer timer =
TraceContext.newTimer(
- "Loading project", Metadata.builder().projectName(projectName).build())) {
- long now = clock.read();
- Project.NameKey key = Project.nameKey(projectName);
+ "Loading project", Metadata.builder().projectName(key.get()).build())) {
try (Repository git = mgr.openRepository(key)) {
ProjectConfig cfg = projectConfigFactory.create(key);
cfg.load(key, git);
-
- ProjectState state = projectStateFactory.create(cfg);
- state.initLastCheck(now);
- return state;
+ return projectStateFactory.create(cfg);
}
}
}
+
+ @Override
+ public ListenableFuture<ProjectState> reload(Project.NameKey key, ProjectState oldState)
+ throws Exception {
+ try (TraceTimer timer =
+ TraceContext.newTimer(
+ "Reload project", Metadata.builder().projectName(key.get()).build())) {
+ try (Repository git = mgr.openRepository(key)) {
+ Ref configRef = git.exactRef(RefNames.REFS_CONFIG);
+ if (configRef != null
+ && configRef.getObjectId().equals(oldState.getConfig().getRevision())) {
+ refreshCounter.increment(CACHE_NAME, false);
+ return Futures.immediateFuture(oldState);
+ }
+ }
+
+ // Repository is not thread safe, so we have to open it on the thread that does the loading.
+ // Just invoke the loader on the other thread.
+ refreshCounter.increment(CACHE_NAME, true);
+ return cacheRefreshExecutor.submit(() -> load(key));
+ }
+ }
}
static class ListKey {
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/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index e52f344..efadcc8 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -32,7 +32,6 @@
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -59,7 +58,6 @@
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
/**
@@ -86,9 +84,6 @@
private final long globalMaxObjectSizeLimit;
private final boolean inheritProjectMaxObjectSizeLimit;
- /** Last system time the configuration's revision was examined. */
- private volatile long lastCheckGeneration;
-
/** Local access sections, wrapped in SectionMatchers for faster evaluation. */
private volatile List<SectionMatcher> localAccessSections;
@@ -140,33 +135,6 @@
}
}
- void initLastCheck(long generation) {
- lastCheckGeneration = generation;
- }
-
- boolean needsRefresh(long generation) {
- if (generation <= 0) {
- return isRevisionOutOfDate();
- }
- if (lastCheckGeneration != generation) {
- lastCheckGeneration = generation;
- return isRevisionOutOfDate();
- }
- return false;
- }
-
- private boolean isRevisionOutOfDate() {
- try (Repository git = gitMgr.openRepository(getNameKey())) {
- Ref ref = git.getRefDatabase().exactRef(RefNames.REFS_CONFIG);
- if (ref == null || ref.getObjectId() == null) {
- return true;
- }
- return !ref.getObjectId().equals(config.getRevision());
- } catch (IOException gone) {
- return true;
- }
- }
-
/**
* @return cached computation of all global capabilities. This should only be invoked on the state
* from {@link ProjectCache#getAllProjects()}. Null on any other project.
diff --git a/java/com/google/gerrit/server/restapi/account/CreateEmail.java b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
index fee5eab..89b931f 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
@@ -37,6 +37,7 @@
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
import com.google.gerrit.server.mail.send.RegisterNewEmailSender;
import com.google.gerrit.server.permissions.GlobalPermission;
@@ -80,6 +81,7 @@
private final RegisterNewEmailSender.Factory registerNewEmailFactory;
private final PutPreferred putPreferred;
private final OutgoingEmailValidator validator;
+ private final MessageIdGenerator messageIdGenerator;
private final boolean isDevMode;
@Inject
@@ -91,7 +93,8 @@
AccountManager accountManager,
RegisterNewEmailSender.Factory registerNewEmailFactory,
PutPreferred putPreferred,
- OutgoingEmailValidator validator) {
+ OutgoingEmailValidator validator,
+ MessageIdGenerator messageIdGenerator) {
this.self = self;
this.realm = realm;
this.permissionBackend = permissionBackend;
@@ -100,6 +103,7 @@
this.putPreferred = putPreferred;
this.validator = validator;
this.isDevMode = authConfig.getAuthType() == DEVELOPMENT_BECOME_ANY_ACCOUNT;
+ this.messageIdGenerator = messageIdGenerator;
}
@Override
@@ -161,6 +165,7 @@
if (!sender.isAllowed()) {
throw new MethodNotAllowedException("Not allowed to add email address " + email);
}
+ sender.setMessageId(messageIdGenerator.fromAccountUpdate(user.getAccountId()));
sender.send();
info.pendingConfirmation = true;
} catch (EmailException | RuntimeException e) {
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVote.java b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
index 4c39763..bfe7177 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -44,6 +44,7 @@
import com.google.gerrit.server.change.VoteResource;
import com.google.gerrit.server.extensions.events.VoteDeleted;
import com.google.gerrit.server.mail.send.DeleteVoteSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.mail.send.ReplyToChangeSender;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectCache;
@@ -77,6 +78,7 @@
private final NotifyResolver notifyResolver;
private final RemoveReviewerControl removeReviewerControl;
private final ProjectCache projectCache;
+ private final MessageIdGenerator messageIdGenerator;
@Inject
DeleteVote(
@@ -89,7 +91,8 @@
DeleteVoteSender.Factory deleteVoteSenderFactory,
NotifyResolver notifyResolver,
RemoveReviewerControl removeReviewerControl,
- ProjectCache projectCache) {
+ ProjectCache projectCache,
+ MessageIdGenerator messageIdGenerator) {
this.updateFactory = updateFactory;
this.approvalsUtil = approvalsUtil;
this.psUtil = psUtil;
@@ -100,6 +103,7 @@
this.notifyResolver = notifyResolver;
this.removeReviewerControl = removeReviewerControl;
this.projectCache = projectCache;
+ this.messageIdGenerator = messageIdGenerator;
}
@Override
@@ -229,6 +233,8 @@
cm.setFrom(user.getAccountId());
cm.setChangeMessage(changeMessage.getMessage(), ctx.getWhen());
cm.setNotify(notify);
+ cm.setMessageId(
+ messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
cm.send();
}
} catch (Exception e) {
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 1e2e644..a16d4f9 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -49,11 +49,11 @@
import com.google.gerrit.entities.Comment;
import com.google.gerrit.entities.FixReplacement;
import com.google.gerrit.entities.FixSuggestion;
-import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.RobotComment;
+import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
import com.google.gerrit.extensions.api.changes.AddReviewerResult;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -904,9 +904,23 @@
}
NotifyResolver.Result notify = ctx.getNotify(notes.getChangeId());
if (notify.shouldNotify()) {
- email
- .create(notify, notes, ps, user, message, comments, in.message, labelDelta)
- .sendAsync();
+ try {
+ email
+ .create(
+ notify,
+ notes,
+ ps,
+ user,
+ message,
+ comments,
+ in.message,
+ labelDelta,
+ ctx.getRepoView())
+ .sendAsync();
+ } catch (IOException ex) {
+ throw new StorageException(
+ String.format("Repository %s not found", ctx.getProject().get()), ex);
+ }
}
commentAdded.fire(
notes.getChange(),
@@ -1271,8 +1285,6 @@
return false;
}
- forceCallerAsReviewer(projectState, ctx, current, ups, del);
-
return !del.isEmpty() || !ups.isEmpty();
}
@@ -1343,41 +1355,6 @@
}
}
- private void forceCallerAsReviewer(
- ProjectState projectState,
- ChangeContext ctx,
- Map<String, PatchSetApproval> current,
- List<PatchSetApproval> ups,
- List<PatchSetApproval> del) {
- if (current.isEmpty() && ups.isEmpty()) {
- // TODO Find another way to link reviewers to changes.
- if (del.isEmpty()) {
- // If no existing label is being set to 0, hack in the caller
- // as a reviewer by picking the first server-wide LabelType.
- List<LabelType> labelTypes = projectState.getLabelTypes(ctx.getNotes()).getLabelTypes();
- if (labelTypes.isEmpty()) {
- logger.atWarning().log(
- "no label type found for project %s, change %s",
- projectState.getName(), ctx.getChange().getChangeId());
- return;
- }
-
- LabelId labelId = labelTypes.get(0).getLabelId();
- ups.add(
- ApprovalsUtil.newApproval(psId, user, labelId, 0, ctx.getWhen())
- .tag(Optional.ofNullable(in.tag))
- .granted(ctx.getWhen())
- .build());
- } else {
- // Pick a random label that is about to be deleted and keep it.
- Iterator<PatchSetApproval> i = del.iterator();
- ups.add(i.next().toBuilder().value(0).granted(ctx.getWhen()).build());
- i.remove();
- }
- }
- ctx.getUpdate(ctx.getChange().currentPatchSetId()).putReviewer(user.getAccountId(), REVIEWER);
- }
-
private Map<String, PatchSetApproval> scanLabels(
ProjectState projectState, ChangeContext ctx, List<PatchSetApproval> del)
throws IOException {
diff --git a/java/com/google/gerrit/server/restapi/change/Restore.java b/java/com/google/gerrit/server/restapi/change/Restore.java
index a72192e..d1506b7 100644
--- a/java/com/google/gerrit/server/restapi/change/Restore.java
+++ b/java/com/google/gerrit/server/restapi/change/Restore.java
@@ -36,6 +36,7 @@
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.extensions.events.ChangeRestored;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.mail.send.ReplyToChangeSender;
import com.google.gerrit.server.mail.send.RestoredSender;
import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -65,6 +66,7 @@
private final PatchSetUtil psUtil;
private final ChangeRestored changeRestored;
private final ProjectCache projectCache;
+ private final MessageIdGenerator messageIdGenerator;
@Inject
Restore(
@@ -74,7 +76,8 @@
ChangeMessagesUtil cmUtil,
PatchSetUtil psUtil,
ChangeRestored changeRestored,
- ProjectCache projectCache) {
+ ProjectCache projectCache,
+ MessageIdGenerator messageIdGenerator) {
this.updateFactory = updateFactory;
this.restoredSenderFactory = restoredSenderFactory;
this.json = json;
@@ -82,6 +85,7 @@
this.psUtil = psUtil;
this.changeRestored = changeRestored;
this.projectCache = projectCache;
+ this.messageIdGenerator = messageIdGenerator;
}
@Override
@@ -149,6 +153,8 @@
ReplyToChangeSender cm = restoredSenderFactory.create(ctx.getProject(), change.getId());
cm.setFrom(ctx.getAccountId());
cm.setChangeMessage(message.getMessage(), ctx.getWhen());
+ cm.setMessageId(
+ messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
cm.send();
} catch (Exception e) {
logger.atSevere().withCause(e).log("Cannot email update for change %s", change.getId());
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
index 88db66e..e2b898f 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -58,6 +58,7 @@
import com.google.gerrit.server.extensions.events.ChangeReverted;
import com.google.gerrit.server.git.CommitUtil;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.mail.send.RevertedSender;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.Sequences;
@@ -126,6 +127,7 @@
private final BatchUpdate.Factory updateFactory;
private final ChangeResource.Factory changeResourceFactory;
private final GetRelated getRelated;
+ private final MessageIdGenerator messageIdGenerator;
private CherryPickInput cherryPickInput;
private List<ChangeInfo> results;
@@ -154,7 +156,8 @@
NotifyResolver notifyResolver,
BatchUpdate.Factory updateFactory,
ChangeResource.Factory changeResourceFactory,
- GetRelated getRelated) {
+ GetRelated getRelated,
+ MessageIdGenerator messageIdGenerator) {
this.queryProvider = queryProvider;
this.user = user;
this.permissionBackend = permissionBackend;
@@ -175,6 +178,7 @@
this.updateFactory = updateFactory;
this.changeResourceFactory = changeResourceFactory;
this.getRelated = getRelated;
+ this.messageIdGenerator = messageIdGenerator;
results = new ArrayList<>();
cherryPickInput = null;
}
@@ -601,6 +605,8 @@
RevertedSender cm = revertedSenderFactory.create(ctx.getProject(), change.getId());
cm.setFrom(ctx.getAccountId());
cm.setNotify(ctx.getNotify(change.getId()));
+ cm.setMessageId(
+ messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
cm.send();
} catch (Exception err) {
logger.atSevere().withCause(err).log(
diff --git a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
index 8362e95..c118766 100644
--- a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
+++ b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
@@ -75,8 +75,8 @@
@Override
public Description getDescription(ChangeResource rsrc) {
return new Description()
- .setLabel("Start Review")
- .setTitle("Set Ready For Review")
+ .setLabel("Mark as Active")
+ .setTitle("Switch change state from WIP to Active (ready for review)")
.setVisible(
and(
rsrc.getChange().isNew() && rsrc.getChange().isWorkInProgress(),
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/AclUtil.java b/java/com/google/gerrit/server/schema/AclUtil.java
index f0aafef..f6c3aad 100644
--- a/java/com/google/gerrit/server/schema/AclUtil.java
+++ b/java/com/google/gerrit/server/schema/AclUtil.java
@@ -105,4 +105,15 @@
public static PermissionRule rule(ProjectConfig config, GroupReference group) {
return new PermissionRule(config.resolve(group));
}
+
+ public static void remove(
+ ProjectConfig config, AccessSection section, String permission, GroupReference... groupList) {
+ Permission p = section.getPermission(permission, true);
+ for (GroupReference group : groupList) {
+ if (group != null) {
+ PermissionRule r = rule(config, group);
+ p.remove(r);
+ }
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index c19be5e..cfa5825 100644
--- a/java/com/google/gerrit/server/schema/AllProjectsCreator.java
+++ b/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -174,11 +174,12 @@
AccessSection heads, LabelType codeReviewLabel, ProjectConfig config) {
AccessSection refsFor = config.getAccessSection("refs/for/*", true);
AccessSection magic = config.getAccessSection("refs/for/" + AccessSection.ALL, true);
+ AccessSection all = config.getAccessSection("refs/*", true);
grant(config, refsFor, Permission.ADD_PATCH_SET, registered);
grant(config, heads, codeReviewLabel, -1, 1, registered);
grant(config, heads, Permission.FORGE_AUTHOR, registered);
- grant(config, heads, Permission.REVERT, registered);
+ grant(config, all, Permission.REVERT, registered);
grant(config, magic, Permission.PUSH, registered);
grant(config, magic, Permission.PUSH_MERGE, registered);
}
diff --git a/java/com/google/gerrit/server/schema/GrantRevertPermission.java b/java/com/google/gerrit/server/schema/GrantRevertPermission.java
index d6228ce..2f890d5 100644
--- a/java/com/google/gerrit/server/schema/GrantRevertPermission.java
+++ b/java/com/google/gerrit/server/schema/GrantRevertPermission.java
@@ -16,6 +16,7 @@
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.server.schema.AclUtil.grant;
+import static com.google.gerrit.server.schema.AclUtil.remove;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GroupReference;
@@ -64,12 +65,26 @@
ProjectConfig projectConfig = projectConfigFactory.read(md);
AccessSection heads = projectConfig.getAccessSection(AccessSection.HEADS, true);
- Permission permission = heads.getPermission(Permission.REVERT);
- if (permission != null && permission.getRule(registeredUsers) != null) {
- // permission already exists, don't do anything.
+ Permission permissionOnRefsHeads = heads.getPermission(Permission.REVERT);
+
+ if (permissionOnRefsHeads != null) {
+ if (permissionOnRefsHeads.getRule(registeredUsers) == null
+ || permissionOnRefsHeads.getRules().size() > 1) {
+ // If admins already changed the permission, don't do anything.
+ return;
+ }
+ // permission already exists in refs/heads/*, delete it for Registered Users.
+ remove(projectConfig, heads, Permission.REVERT, registeredUsers);
+ }
+
+ AccessSection all = projectConfig.getAccessSection(AccessSection.ALL, true);
+ Permission permissionOnRefsStar = all.getPermission(Permission.REVERT);
+ if (permissionOnRefsStar != null && permissionOnRefsStar.getRule(registeredUsers) != null) {
+ // permission already exists in refs/*, don't do anything.
return;
}
- grant(projectConfig, heads, Permission.REVERT, registeredUsers);
+ // If the permission doesn't exist of refs/* for Registered Users, grant it.
+ grant(projectConfig, all, Permission.REVERT, registeredUsers);
md.getCommitBuilder().setAuthor(serverUser);
md.getCommitBuilder().setCommitter(serverUser);
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/schema/Schema_182.java b/java/com/google/gerrit/server/schema/Schema_182.java
index 3928b2b..a61a175 100644
--- a/java/com/google/gerrit/server/schema/Schema_182.java
+++ b/java/com/google/gerrit/server/schema/Schema_182.java
@@ -28,7 +28,8 @@
public void upgrade(Arguments args, UpdateUI ui) throws Exception {
AllUsersName allUsers = args.allUsers;
GitRepositoryManager gitRepoManager = args.repoManager;
- DeleteZombieCommentsRefs cleanup = new DeleteZombieCommentsRefs(allUsers, gitRepoManager, 100);
+ DeleteZombieCommentsRefs cleanup =
+ new DeleteZombieCommentsRefs(allUsers, gitRepoManager, 100, ui::message);
cleanup.execute();
}
}
diff --git a/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java b/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java
index 606d851..8b159bc 100644
--- a/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java
+++ b/java/com/google/gerrit/server/schema/testing/AllProjectsCreatorTestUtil.java
@@ -55,6 +55,7 @@
"[access \"refs/*\"]",
" read = group Administrators",
" read = group Anonymous Users",
+ " revert = group Registered Users",
"[access \"refs/for/*\"]",
" addPatchSet = group Registered Users",
"[access \"refs/for/refs/*\"]",
@@ -75,7 +76,6 @@
" push = group Project Owners",
" submit = group Administrators",
" submit = group Project Owners",
- " revert = group Registered Users",
"[access \"refs/meta/config\"]",
" exclusiveGroupPermissions = read",
" create = group Administrators",
diff --git a/java/com/google/gerrit/server/ssh/SshAddressesModule.java b/java/com/google/gerrit/server/ssh/SshAddressesModule.java
index 0a6bcac..1159e06 100644
--- a/java/com/google/gerrit/server/ssh/SshAddressesModule.java
+++ b/java/com/google/gerrit/server/ssh/SshAddressesModule.java
@@ -40,7 +40,7 @@
@Provides
@Singleton
@SshListenAddresses
- public List<SocketAddress> getListenAddresses(@GerritServerConfig Config cfg) {
+ public List<SocketAddress> provideListenAddresses(@GerritServerConfig Config cfg) {
List<SocketAddress> listen = Lists.newArrayListWithExpectedSize(2);
String[] want = cfg.getStringList("sshd", null, "listenaddress");
if (want == null || want.length == 0) {
@@ -71,7 +71,7 @@
@Provides
@Singleton
@SshAdvertisedAddresses
- List<String> getAdvertisedAddresses(
+ List<String> provideAdvertisedAddresses(
@GerritServerConfig Config cfg, @SshListenAddresses List<SocketAddress> listen) {
String[] want = cfg.getStringList("sshd", null, "advertisedaddress");
if (want.length > 0) {
diff --git a/java/com/google/gerrit/server/submit/BranchTips.java b/java/com/google/gerrit/server/submit/BranchTips.java
new file mode 100644
index 0000000..d42517c
--- /dev/null
+++ b/java/com/google/gerrit/server/submit/BranchTips.java
@@ -0,0 +1,62 @@
+// 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.submit;
+
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Ref;
+
+/**
+ * Current branch tips, taking into account commits created during the submit process as well as
+ * submodule updates produced by this class.
+ */
+class BranchTips {
+
+ private final Map<BranchNameKey, CodeReviewCommit> branchTips = new HashMap<>();
+
+ /**
+ * Returns current tip of the branch, taking into account commits created during the submit
+ * process or submodule updates.
+ *
+ * @param branch branch
+ * @param repo repository to look for the branch if not cached
+ * @return the current tip. Empty if the branch doesn't exist in the repository
+ * @throws IOException Cannot access the underlying storage
+ */
+ Optional<CodeReviewCommit> getTip(BranchNameKey branch, OpenRepo repo) throws IOException {
+ CodeReviewCommit currentCommit;
+ if (branchTips.containsKey(branch)) {
+ currentCommit = branchTips.get(branch);
+ } else {
+ Ref r = repo.repo.exactRef(branch.branch());
+ if (r == null) {
+ return Optional.empty();
+ }
+ currentCommit = repo.rw.parseCommit(r.getObjectId());
+ branchTips.put(branch, currentCommit);
+ }
+
+ return Optional.of(currentCommit);
+ }
+
+ void put(BranchNameKey branch, CodeReviewCommit c) {
+ branchTips.put(branch, c);
+ }
+}
diff --git a/java/com/google/gerrit/server/submit/CircularPathFinder.java b/java/com/google/gerrit/server/submit/CircularPathFinder.java
new file mode 100644
index 0000000..d1920da
--- /dev/null
+++ b/java/com/google/gerrit/server/submit/CircularPathFinder.java
@@ -0,0 +1,41 @@
+// 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.submit;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+class CircularPathFinder {
+ private CircularPathFinder() {}
+
+ /**
+ * Prints a circular path according to the nodes in {@code p} and the start node {@code target}.
+ */
+ public static <T> String printCircularPath(Collection<T> p, T target) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(target);
+ ArrayList<T> reverseP = new ArrayList<>(p);
+ Collections.reverse(reverseP);
+ for (T t : reverseP) {
+ sb.append("->");
+ sb.append(t);
+ if (t.equals(target)) {
+ break;
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/java/com/google/gerrit/server/submit/EmailMerge.java b/java/com/google/gerrit/server/submit/EmailMerge.java
index c94d49e..c433ee6 100644
--- a/java/com/google/gerrit/server/submit/EmailMerge.java
+++ b/java/com/google/gerrit/server/submit/EmailMerge.java
@@ -24,6 +24,8 @@
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.config.SendEmailExecutor;
import com.google.gerrit.server.mail.send.MergedSender;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
+import com.google.gerrit.server.update.RepoView;
import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.inject.Inject;
@@ -38,20 +40,23 @@
interface Factory {
EmailMerge create(
Project.NameKey project,
- Change.Id changeId,
+ Change change,
Account.Id submitter,
- NotifyResolver.Result notify);
+ NotifyResolver.Result notify,
+ RepoView repoView);
}
private final ExecutorService sendEmailsExecutor;
private final MergedSender.Factory mergedSenderFactory;
private final ThreadLocalRequestContext requestContext;
private final IdentifiedUser.GenericFactory identifiedUserFactory;
+ private final MessageIdGenerator messageIdGenerator;
private final Project.NameKey project;
- private final Change.Id changeId;
+ private final Change change;
private final Account.Id submitter;
private final NotifyResolver.Result notify;
+ private final RepoView repoView;
@Inject
EmailMerge(
@@ -59,18 +64,22 @@
MergedSender.Factory mergedSenderFactory,
ThreadLocalRequestContext requestContext,
IdentifiedUser.GenericFactory identifiedUserFactory,
+ MessageIdGenerator messageIdGenerator,
@Assisted Project.NameKey project,
- @Assisted Change.Id changeId,
+ @Assisted Change change,
@Assisted @Nullable Account.Id submitter,
- @Assisted NotifyResolver.Result notify) {
+ @Assisted NotifyResolver.Result notify,
+ @Assisted RepoView repoView) {
this.sendEmailsExecutor = executor;
this.mergedSenderFactory = mergedSenderFactory;
this.requestContext = requestContext;
this.identifiedUserFactory = identifiedUserFactory;
+ this.messageIdGenerator = messageIdGenerator;
this.project = project;
- this.changeId = changeId;
+ this.change = change;
this.submitter = submitter;
this.notify = notify;
+ this.repoView = repoView;
}
void sendAsync() {
@@ -82,14 +91,15 @@
public void run() {
RequestContext old = requestContext.setContext(this);
try {
- MergedSender cm = mergedSenderFactory.create(project, changeId);
+ MergedSender cm = mergedSenderFactory.create(project, change.getId());
if (submitter != null) {
cm.setFrom(submitter);
}
cm.setNotify(notify);
+ cm.setMessageId(messageIdGenerator.fromChangeUpdate(repoView, change.currentPatchSetId()));
cm.send();
} catch (Exception e) {
- logger.atSevere().withCause(e).log("Cannot email merged notification for %s", changeId);
+ logger.atSevere().withCause(e).log("Cannot email merged notification for %s", change.getId());
} finally {
requestContext.setContext(old);
}
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index a4141be..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 "";
}
@@ -500,7 +501,12 @@
// have failed fast in one of the other steps.
try {
args.mergedSenderFactory
- .create(ctx.getProject(), getId(), submitter.accountId(), ctx.getNotify(getId()))
+ .create(
+ ctx.getProject(),
+ toMerge.change(),
+ submitter.accountId(),
+ ctx.getNotify(getId()),
+ ctx.getRepoView())
.sendAsync();
} catch (Exception e) {
logger.atSevere().withCause(e).log("Cannot email merged notification for %s", getId());
@@ -549,7 +555,7 @@
// Modify the commit with gitlink update
try {
- return args.submoduleOp.composeGitlinksCommit(args.destBranch, commit);
+ return args.submoduleOp.amendGitlinksCommit(args.destBranch, commit);
} catch (IOException e) {
throw new StorageException(
String.format("cannot update gitlink for the commit at branch %s", args.destBranch), e);
diff --git a/java/com/google/gerrit/server/submit/SubmoduleOp.java b/java/com/google/gerrit/server/submit/SubmoduleOp.java
index b48076194..5adda2c 100644
--- a/java/com/google/gerrit/server/submit/SubmoduleOp.java
+++ b/java/com/google/gerrit/server/submit/SubmoduleOp.java
@@ -14,19 +14,14 @@
package com.google.gerrit.server.submit;
-import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.MultimapBuilder;
-import com.google.common.collect.SetMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.UsedAt;
-import com.google.gerrit.common.data.SubscribeSection;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.SubmoduleSubscription;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -46,18 +41,13 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
-import java.util.Deque;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jgit.dircache.DirCache;
@@ -71,10 +61,8 @@
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.RefSpec;
public class SubmoduleOp {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -82,9 +70,11 @@
/** Only used for branches without code review changes */
public class GitlinkOp implements RepoOnlyOp {
private final BranchNameKey branch;
+ private final BranchTips currentBranchTips;
- GitlinkOp(BranchNameKey branch) {
+ GitlinkOp(BranchNameKey branch, BranchTips branchTips) {
this.branch = branch;
+ this.currentBranchTips = branchTips;
}
@Override
@@ -92,7 +82,7 @@
CodeReviewCommit c = composeGitlinksCommit(branch);
if (c != null) {
ctx.addRefUpdate(c.getParent(0), c, branch.branch());
- addBranchTip(branch, c);
+ currentBranchTips.put(branch, c);
}
}
}
@@ -123,42 +113,15 @@
}
}
- private final GitModules.Factory gitmodulesFactory;
private final PersonIdent myIdent;
- private final ProjectCache projectCache;
private final VerboseSuperprojectUpdate verboseSuperProject;
- private final boolean enableSuperProjectSubscriptions;
private final long maxCombinedCommitMessageSize;
private final long maxCommitMessages;
private final MergeOpRepoManager orm;
- private final Map<BranchNameKey, GitModules> branchGitModules;
+ private final SubscriptionGraph.Factory subscriptionGraphFactory;
+ private final SubscriptionGraph subscriptionGraph;
- /** Branches updated as part of the enclosing submit or push batch. */
- private final ImmutableSet<BranchNameKey> updatedBranches;
-
- /**
- * Current branch tips, taking into account commits created during the submit process as well as
- * submodule updates produced by this class.
- */
- private final Map<BranchNameKey, CodeReviewCommit> branchTips;
-
- /**
- * Branches in a superproject that contain submodule subscriptions, plus branches in submodules
- * which are subscribed to by some superproject.
- */
- private final Set<BranchNameKey> affectedBranches;
-
- /** Copy of {@link #affectedBranches}, sorted by submodule traversal order. */
- private final ImmutableSet<BranchNameKey> sortedBranches;
-
- /** Multimap of superproject branch to submodule subscriptions contained in that branch. */
- private final SetMultimap<BranchNameKey, SubmoduleSubscription> targets;
-
- /**
- * Multimap of superproject name to all branch names within that superproject which have submodule
- * subscriptions.
- */
- private final SetMultimap<Project.NameKey, BranchNameKey> branchesByProject;
+ private final BranchTips branchTips = new BranchTips();
private SubmoduleOp(
GitModules.Factory gitmodulesFactory,
@@ -168,233 +131,27 @@
Set<BranchNameKey> updatedBranches,
MergeOpRepoManager orm)
throws SubmoduleConflictException {
- this.gitmodulesFactory = gitmodulesFactory;
this.myIdent = myIdent;
- this.projectCache = projectCache;
this.verboseSuperProject =
cfg.getEnum("submodule", null, "verboseSuperprojectUpdate", VerboseSuperprojectUpdate.TRUE);
- this.enableSuperProjectSubscriptions =
- cfg.getBoolean("submodule", "enableSuperProjectSubscriptions", true);
this.maxCombinedCommitMessageSize =
cfg.getLong("submodule", "maxCombinedCommitMessageSize", 256 << 10);
this.maxCommitMessages = cfg.getLong("submodule", "maxCommitMessages", 1000);
this.orm = orm;
- this.updatedBranches = ImmutableSet.copyOf(updatedBranches);
- this.targets = MultimapBuilder.hashKeys().hashSetValues().build();
- this.affectedBranches = new HashSet<>();
- this.branchTips = new HashMap<>();
- this.branchGitModules = new HashMap<>();
- this.branchesByProject = MultimapBuilder.hashKeys().hashSetValues().build();
- this.sortedBranches = calculateSubscriptionMaps();
- }
-
- /**
- * Calculate the internal maps used by the operation.
- *
- * <p>In addition to the return value, the following fields are populated as a side effect:
- *
- * <ul>
- * <li>{@link #affectedBranches}
- * <li>{@link #targets}
- * <li>{@link #branchesByProject}
- * </ul>
- *
- * @return the ordered set to be stored in {@link #sortedBranches}.
- */
- // TODO(dborowitz): This setup process is hard to follow, in large part due to the accumulation of
- // mutable maps, which makes this whole class difficult to understand.
- //
- // A cleaner architecture for this process might be:
- // 1. Separate out the code to parse submodule subscriptions and build up an in-memory data
- // structure representing the subscription graph, using a separate class with a properly-
- // documented interface.
- // 2. Walk the graph to produce a work plan. This would be a list of items indicating: create a
- // commit in project X reading branch tips for submodules S1..Sn and updating gitlinks in X.
- // 3. Execute the work plan, i.e. convert the items into BatchUpdate.Ops and add them to the
- // relevant updates.
- //
- // In addition to improving readability, this approach has the advantage of making (1) and (2)
- // testable using small tests.
- private ImmutableSet<BranchNameKey> calculateSubscriptionMaps()
- throws SubmoduleConflictException {
- if (!enableSuperProjectSubscriptions) {
+ this.subscriptionGraphFactory =
+ new SubscriptionGraph.DefaultFactory(gitmodulesFactory, projectCache, orm);
+ if (cfg.getBoolean("submodule", "enableSuperProjectSubscriptions", true)) {
+ this.subscriptionGraph = subscriptionGraphFactory.compute(updatedBranches);
+ } else {
logger.atFine().log("Updating superprojects disabled");
- return null;
+ this.subscriptionGraph =
+ SubscriptionGraph.createEmptyGraph(ImmutableSet.copyOf(updatedBranches));
}
-
- logger.atFine().log("Calculating superprojects - submodules map");
- LinkedHashSet<BranchNameKey> allVisited = new LinkedHashSet<>();
- for (BranchNameKey updatedBranch : updatedBranches) {
- if (allVisited.contains(updatedBranch)) {
- continue;
- }
-
- searchForSuperprojects(updatedBranch, new LinkedHashSet<>(), allVisited);
- }
-
- // Since the searchForSuperprojects will add all branches (related or
- // unrelated) and ensure the superproject's branches get added first before
- // a submodule branch. Need remove all unrelated branches and reverse
- // the order.
- allVisited.retainAll(affectedBranches);
- reverse(allVisited);
- return ImmutableSet.copyOf(allVisited);
- }
-
- private void searchForSuperprojects(
- BranchNameKey current,
- LinkedHashSet<BranchNameKey> currentVisited,
- LinkedHashSet<BranchNameKey> allVisited)
- throws SubmoduleConflictException {
- logger.atFine().log("Now processing %s", current);
-
- if (currentVisited.contains(current)) {
- throw new SubmoduleConflictException(
- "Branch level circular subscriptions detected: "
- + printCircularPath(currentVisited, current));
- }
-
- if (allVisited.contains(current)) {
- return;
- }
-
- currentVisited.add(current);
- try {
- Collection<SubmoduleSubscription> subscriptions =
- superProjectSubscriptionsForSubmoduleBranch(current);
- for (SubmoduleSubscription sub : subscriptions) {
- BranchNameKey superBranch = sub.getSuperProject();
- searchForSuperprojects(superBranch, currentVisited, allVisited);
- targets.put(superBranch, sub);
- branchesByProject.put(superBranch.project(), superBranch);
- affectedBranches.add(superBranch);
- affectedBranches.add(sub.getSubmodule());
- }
- } catch (IOException e) {
- throw new StorageException("Cannot find superprojects for " + current, e);
- }
- currentVisited.remove(current);
- allVisited.add(current);
- }
-
- private static <T> void reverse(LinkedHashSet<T> set) {
- if (set == null) {
- return;
- }
-
- Deque<T> q = new ArrayDeque<>(set);
- set.clear();
-
- while (!q.isEmpty()) {
- set.add(q.removeLast());
- }
- }
-
- private <T> String printCircularPath(LinkedHashSet<T> p, T target) {
- StringBuilder sb = new StringBuilder();
- sb.append(target);
- ArrayList<T> reverseP = new ArrayList<>(p);
- Collections.reverse(reverseP);
- for (T t : reverseP) {
- sb.append("->");
- sb.append(t);
- if (t.equals(target)) {
- break;
- }
- }
- return sb.toString();
- }
-
- private Collection<BranchNameKey> getDestinationBranches(BranchNameKey src, SubscribeSection s)
- throws IOException {
- Collection<BranchNameKey> ret = new HashSet<>();
- logger.atFine().log("Inspecting SubscribeSection %s", s);
- for (RefSpec r : s.getMatchingRefSpecs()) {
- logger.atFine().log("Inspecting [matching] ref %s", r);
- if (!r.matchSource(src.branch())) {
- continue;
- }
- if (r.isWildcard()) {
- // refs/heads/*[:refs/somewhere/*]
- ret.add(
- BranchNameKey.create(
- s.getProject(), r.expandFromSource(src.branch()).getDestination()));
- } else {
- // e.g. refs/heads/master[:refs/heads/stable]
- String dest = r.getDestination();
- if (dest == null) {
- dest = r.getSource();
- }
- ret.add(BranchNameKey.create(s.getProject(), dest));
- }
- }
-
- for (RefSpec r : s.getMultiMatchRefSpecs()) {
- logger.atFine().log("Inspecting [all] ref %s", r);
- if (!r.matchSource(src.branch())) {
- continue;
- }
- OpenRepo or;
- try {
- or = orm.getRepo(s.getProject());
- } catch (NoSuchProjectException e) {
- // A project listed a non existent project to be allowed
- // to subscribe to it. Allow this for now, i.e. no exception is
- // thrown.
- continue;
- }
-
- for (Ref ref : or.repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_HEADS)) {
- if (r.getDestination() != null && !r.matchDestination(ref.getName())) {
- continue;
- }
- BranchNameKey b = BranchNameKey.create(s.getProject(), ref.getName());
- if (!ret.contains(b)) {
- ret.add(b);
- }
- }
- }
- logger.atFine().log("Returning possible branches: %s for project %s", ret, s.getProject());
- return ret;
}
@UsedAt(UsedAt.Project.PLUGIN_DELETE_PROJECT)
- public Collection<SubmoduleSubscription> superProjectSubscriptionsForSubmoduleBranch(
- BranchNameKey srcBranch) throws IOException {
- logger.atFine().log("Calculating possible superprojects for %s", srcBranch);
- Collection<SubmoduleSubscription> ret = new ArrayList<>();
- Project.NameKey srcProject = srcBranch.project();
- for (SubscribeSection s :
- projectCache
- .get(srcProject)
- .orElseThrow(illegalState(srcProject))
- .getSubscribeSections(srcBranch)) {
- logger.atFine().log("Checking subscribe section %s", s);
- Collection<BranchNameKey> branches = getDestinationBranches(srcBranch, s);
- for (BranchNameKey targetBranch : branches) {
- Project.NameKey targetProject = targetBranch.project();
- try {
- OpenRepo or = orm.getRepo(targetProject);
- ObjectId id = or.repo.resolve(targetBranch.branch());
- if (id == null) {
- logger.atFine().log("The branch %s doesn't exist.", targetBranch);
- continue;
- }
- } catch (NoSuchProjectException e) {
- logger.atFine().log("The project %s doesn't exist", targetProject);
- continue;
- }
-
- GitModules m = branchGitModules.get(targetBranch);
- if (m == null) {
- m = gitmodulesFactory.create(targetBranch, orm);
- branchGitModules.put(targetBranch, m);
- }
- ret.addAll(m.subscribedTo(srcBranch));
- }
- }
- logger.atFine().log("Calculated superprojects for %s are %s", srcBranch, ret);
- return ret;
+ public boolean hasSuperproject(BranchNameKey branch) {
+ return subscriptionGraph.hasSuperproject(branch);
}
public void updateSuperProjects() throws RestApiException {
@@ -407,11 +164,11 @@
try {
for (Project.NameKey project : projects) {
// only need superprojects
- if (branchesByProject.containsKey(project)) {
+ if (subscriptionGraph.isAffectedSuperProject(project)) {
superProjects.add(project);
// get a new BatchUpdate for the super project
OpenRepo or = orm.getRepo(project);
- for (BranchNameKey branch : branchesByProject.get(project)) {
+ for (BranchNameKey branch : subscriptionGraph.getAffectedSuperBranches(project)) {
addOp(or.getUpdate(), branch);
}
}
@@ -432,18 +189,13 @@
throw new StorageException("Cannot access superproject", e);
}
- CodeReviewCommit currentCommit;
- if (branchTips.containsKey(subscriber)) {
- currentCommit = branchTips.get(subscriber);
- } else {
- Ref r = or.repo.exactRef(subscriber.branch());
- if (r == null) {
- throw new SubmoduleConflictException(
- "The branch was probably deleted from the subscriber repository");
- }
- currentCommit = or.rw.parseCommit(r.getObjectId());
- addBranchTip(subscriber, currentCommit);
- }
+ CodeReviewCommit currentCommit =
+ branchTips
+ .getTip(subscriber, or)
+ .orElseThrow(
+ () ->
+ new SubmoduleConflictException(
+ "The branch was probably deleted from the subscriber repository"));
StringBuilder msgbuf = new StringBuilder();
PersonIdent author = null;
@@ -452,7 +204,7 @@
int count = 0;
List<SubmoduleSubscription> subscriptions =
- targets.get(subscriber).stream()
+ subscriptionGraph.getSubscriptions(subscriber).stream()
.sorted(comparing(SubmoduleSubscription::getPath))
.collect(toList());
for (SubmoduleSubscription s : subscriptions) {
@@ -493,7 +245,7 @@
}
/** Amend an existing commit with gitlink updates */
- CodeReviewCommit composeGitlinksCommit(BranchNameKey subscriber, CodeReviewCommit currentCommit)
+ CodeReviewCommit amendGitlinksCommit(BranchNameKey subscriber, CodeReviewCommit currentCommit)
throws IOException, SubmoduleConflictException {
OpenRepo or;
try {
@@ -505,7 +257,7 @@
StringBuilder msgbuf = new StringBuilder();
DirCache dc = readTree(or.rw, currentCommit);
DirCacheEditor ed = dc.editor();
- for (SubmoduleSubscription s : targets.get(subscriber)) {
+ for (SubmoduleSubscription s : subscriptionGraph.getSubscriptions(subscriber)) {
updateSubmodule(dc, ed, msgbuf, s);
}
ed.finish();
@@ -574,25 +326,15 @@
}
}
- final CodeReviewCommit newCommit;
- if (branchTips.containsKey(s.getSubmodule())) {
- // This submodule's branch was updated as part of this specific submit batch: update the
- // gitlink to point to the new commit from the batch.
- newCommit = branchTips.get(s.getSubmodule());
- } else {
- // For whatever reason, this submodule was not updated as part of this submit batch, but the
- // superproject is still subscribed to this branch. Re-read the ref to see if anything has
- // changed since the last time the gitlink was updated, and roll that update into the same
- // commit as all other submodule updates.
- Ref ref = subOr.repo.getRefDatabase().exactRef(s.getSubmodule().branch());
- if (ref == null) {
- ed.add(new DeletePath(s.getPath()));
- return null;
- }
- newCommit = subOr.rw.parseCommit(ref.getObjectId());
- addBranchTip(s.getSubmodule(), newCommit);
+ Optional<CodeReviewCommit> maybeNewCommit = branchTips.getTip(s.getSubmodule(), subOr);
+ if (!maybeNewCommit.isPresent()) {
+ // This submodule branch is neither in the submit set nor in the repository itself
+ ed.add(new DeletePath(s.getPath()));
+ return null;
}
+ CodeReviewCommit newCommit = maybeNewCommit.get();
+
if (Objects.equals(newCommit, oldCommit)) {
// gitlink have already been updated for this submodule
return null;
@@ -678,11 +420,11 @@
ImmutableSet<Project.NameKey> getProjectsInOrder() throws SubmoduleConflictException {
LinkedHashSet<Project.NameKey> projects = new LinkedHashSet<>();
- for (Project.NameKey project : branchesByProject.keySet()) {
+ for (Project.NameKey project : subscriptionGraph.getAffectedSuperProjects()) {
addAllSubmoduleProjects(project, new LinkedHashSet<>(), projects);
}
- for (BranchNameKey branch : updatedBranches) {
+ for (BranchNameKey branch : subscriptionGraph.getUpdatedBranches()) {
projects.add(branch.project());
}
return ImmutableSet.copyOf(projects);
@@ -695,7 +437,8 @@
throws SubmoduleConflictException {
if (current.contains(project)) {
throw new SubmoduleConflictException(
- "Project level circular subscriptions detected: " + printCircularPath(current, project));
+ "Project level circular subscriptions detected: "
+ + CircularPathFinder.printCircularPath(current, project));
}
if (projects.contains(project)) {
@@ -704,8 +447,8 @@
current.add(project);
Set<Project.NameKey> subprojects = new HashSet<>();
- for (BranchNameKey branch : branchesByProject.get(project)) {
- Collection<SubmoduleSubscription> subscriptions = targets.get(branch);
+ for (BranchNameKey branch : subscriptionGraph.getAffectedSuperBranches(project)) {
+ Collection<SubmoduleSubscription> subscriptions = subscriptionGraph.getSubscriptions(branch);
for (SubmoduleSubscription s : subscriptions) {
subprojects.add(s.getSubmodule().project());
}
@@ -721,15 +464,13 @@
ImmutableSet<BranchNameKey> getBranchesInOrder() {
LinkedHashSet<BranchNameKey> branches = new LinkedHashSet<>();
- if (sortedBranches != null) {
- branches.addAll(sortedBranches);
- }
- branches.addAll(updatedBranches);
+ branches.addAll(subscriptionGraph.getSortedSuperprojectAndSubmoduleBranches());
+ branches.addAll(subscriptionGraph.getUpdatedBranches());
return ImmutableSet.copyOf(branches);
}
boolean hasSubscription(BranchNameKey branch) {
- return targets.containsKey(branch);
+ return subscriptionGraph.hasSubscription(branch);
}
void addBranchTip(BranchNameKey branch, CodeReviewCommit tip) {
@@ -737,6 +478,6 @@
}
void addOp(BatchUpdate bu, BranchNameKey branch) {
- bu.addRepoOnlyOp(new GitlinkOp(branch));
+ bu.addRepoOnlyOp(new GitlinkOp(branch, branchTips));
}
}
diff --git a/java/com/google/gerrit/server/submit/SubscriptionGraph.java b/java/com/google/gerrit/server/submit/SubscriptionGraph.java
new file mode 100644
index 0000000..406d878
--- /dev/null
+++ b/java/com/google/gerrit/server/submit/SubscriptionGraph.java
@@ -0,0 +1,375 @@
+// 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.submit;
+
+import static com.google.gerrit.server.project.ProjectCache.illegalState;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.SetMultimap;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.data.SubscribeSection;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.SubmoduleSubscription;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.transport.RefSpec;
+
+/**
+ * A container which stores subscription relationship. A SubscriptionGraph is calculated every time
+ * changes are pushed. Some branches are updated in these changes, and if these branches are
+ * subscribed by other projects, SubscriptionGraph would record information about these updated
+ * branches and branches/projects affected.
+ */
+public class SubscriptionGraph {
+ /** Branches updated as part of the enclosing submit or push batch. */
+ private final ImmutableSet<BranchNameKey> updatedBranches;
+
+ /**
+ * All branches affected, including those in superprojects and submodules, sorted by submodule
+ * traversal order. To support nested subscriptions, GitLink commits need to be updated in order.
+ * The closer to topological "leaf", the earlier a commit should be updated.
+ *
+ * <p>For example, there are three projects, top level project p1 subscribed to p2, p2 subscribed
+ * to bottom level project p3. When submit a change for p3. We need update both p2 and p1. To be
+ * more precise, we need update p2 first and then update p1.
+ */
+ private final ImmutableSet<BranchNameKey> sortedBranches;
+
+ /** Multimap of superproject branch to submodule subscriptions contained in that branch. */
+ private final ImmutableSetMultimap<BranchNameKey, SubmoduleSubscription> targets;
+
+ /**
+ * Multimap of superproject name to all branch names within that superproject which have submodule
+ * subscriptions.
+ */
+ private final ImmutableSetMultimap<Project.NameKey, BranchNameKey> branchesByProject;
+
+ /** All branches subscribed by other projects. */
+ private final ImmutableSet<BranchNameKey> subscribedBranches;
+
+ public SubscriptionGraph(
+ Set<BranchNameKey> updatedBranches,
+ SetMultimap<BranchNameKey, SubmoduleSubscription> targets,
+ SetMultimap<Project.NameKey, BranchNameKey> branchesByProject,
+ Set<BranchNameKey> subscribedBranches,
+ Set<BranchNameKey> sortedBranches) {
+ this.updatedBranches = ImmutableSet.copyOf(updatedBranches);
+ this.targets = ImmutableSetMultimap.copyOf(targets);
+ this.branchesByProject = ImmutableSetMultimap.copyOf(branchesByProject);
+ this.subscribedBranches = ImmutableSet.copyOf(subscribedBranches);
+ this.sortedBranches = ImmutableSet.copyOf(sortedBranches);
+ }
+
+ /** Returns an empty {@code SubscriptionGraph}. */
+ static SubscriptionGraph createEmptyGraph(Set<BranchNameKey> updatedBranches) {
+ return new SubscriptionGraph(
+ updatedBranches,
+ ImmutableSetMultimap.of(),
+ ImmutableSetMultimap.of(),
+ ImmutableSet.of(),
+ ImmutableSet.of());
+ }
+
+ /** Get branches updated as part of the enclosing submit or push batch. */
+ ImmutableSet<BranchNameKey> getUpdatedBranches() {
+ return updatedBranches;
+ }
+
+ /** Get all superprojects affected. */
+ ImmutableSet<Project.NameKey> getAffectedSuperProjects() {
+ return branchesByProject.keySet();
+ }
+
+ /** See if a {@code project} is a superproject affected. */
+ boolean isAffectedSuperProject(Project.NameKey project) {
+ return branchesByProject.containsKey(project);
+ }
+
+ /**
+ * Returns all branches within the superproject {@code project} which have submodule
+ * subscriptions.
+ */
+ ImmutableSet<BranchNameKey> getAffectedSuperBranches(Project.NameKey project) {
+ return branchesByProject.get(project);
+ }
+
+ /**
+ * Get all affected branches, including the submodule branches and superproject branches, sorted
+ * by traversal order.
+ *
+ * @see SubscriptionGraph#sortedBranches
+ */
+ ImmutableSet<BranchNameKey> getSortedSuperprojectAndSubmoduleBranches() {
+ return sortedBranches;
+ }
+
+ /** Check if a {@code branch} is a submodule of a superproject. */
+ boolean hasSuperproject(BranchNameKey branch) {
+ return subscribedBranches.contains(branch);
+ }
+
+ /** See if a {@code branch} is a superproject branch affected. */
+ boolean hasSubscription(BranchNameKey branch) {
+ return targets.containsKey(branch);
+ }
+
+ /** Get all related {@code SubmoduleSubscription}s whose super branch is {@code branch}. */
+ ImmutableSet<SubmoduleSubscription> getSubscriptions(BranchNameKey branch) {
+ return targets.get(branch);
+ }
+
+ public interface Factory {
+ SubscriptionGraph compute(Set<BranchNameKey> updatedBranches) throws SubmoduleConflictException;
+ }
+
+ static class DefaultFactory implements Factory {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private final ProjectCache projectCache;
+ private final GitModules.Factory gitmodulesFactory;
+ private final Map<BranchNameKey, GitModules> branchGitModules;
+ private final MergeOpRepoManager orm;
+
+ // Fields required to the constructor of SubscriptionGraph.
+ /** All affected branches, including those in superprojects and submodules. */
+ private final Set<BranchNameKey> affectedBranches;
+
+ /** @see SubscriptionGraph#targets */
+ private final SetMultimap<BranchNameKey, SubmoduleSubscription> targets;
+
+ /** @see SubscriptionGraph#branchesByProject */
+ private final SetMultimap<Project.NameKey, BranchNameKey> branchesByProject;
+
+ /** @see SubscriptionGraph#subscribedBranches */
+ private final Set<BranchNameKey> subscribedBranches;
+
+ DefaultFactory(
+ GitModules.Factory gitmodulesFactory, ProjectCache projectCache, MergeOpRepoManager orm) {
+ this.gitmodulesFactory = gitmodulesFactory;
+ this.projectCache = projectCache;
+ this.orm = orm;
+ this.branchGitModules = new HashMap<>();
+
+ this.affectedBranches = new HashSet<>();
+ this.targets = MultimapBuilder.hashKeys().hashSetValues().build();
+ this.branchesByProject = MultimapBuilder.hashKeys().hashSetValues().build();
+ this.subscribedBranches = new HashSet<>();
+ }
+
+ @Override
+ public SubscriptionGraph compute(Set<BranchNameKey> updatedBranches)
+ throws SubmoduleConflictException {
+ return new SubscriptionGraph(
+ updatedBranches,
+ targets,
+ branchesByProject,
+ subscribedBranches,
+ calculateSubscriptionMaps(updatedBranches));
+ }
+
+ /**
+ * Calculate the internal maps used by the operation.
+ *
+ * <p>In addition to the return value, the following fields are populated as a side effect:
+ *
+ * <ul>
+ * <li>{@link #affectedBranches}
+ * <li>{@link #targets}
+ * <li>{@link #branchesByProject}
+ * <li>{@link #subscribedBranches}
+ * </ul>
+ *
+ * @return the ordered set to be stored in {@link #sortedBranches}.
+ */
+ private Set<BranchNameKey> calculateSubscriptionMaps(Set<BranchNameKey> updatedBranches)
+ throws SubmoduleConflictException {
+ logger.atFine().log("Calculating superprojects - submodules map");
+ LinkedHashSet<BranchNameKey> allVisited = new LinkedHashSet<>();
+ for (BranchNameKey updatedBranch : updatedBranches) {
+ if (allVisited.contains(updatedBranch)) {
+ continue;
+ }
+
+ searchForSuperprojects(updatedBranch, new LinkedHashSet<>(), allVisited);
+ }
+
+ // Since the searchForSuperprojects will add all branches (related or
+ // unrelated) and ensure the superproject's branches get added first before
+ // a submodule branch. Need remove all unrelated branches and reverse
+ // the order.
+ allVisited.retainAll(affectedBranches);
+ reverse(allVisited);
+ return allVisited;
+ }
+
+ private void searchForSuperprojects(
+ BranchNameKey current,
+ LinkedHashSet<BranchNameKey> currentVisited,
+ LinkedHashSet<BranchNameKey> allVisited)
+ throws SubmoduleConflictException {
+ logger.atFine().log("Now processing %s", current);
+
+ if (currentVisited.contains(current)) {
+ throw new SubmoduleConflictException(
+ "Branch level circular subscriptions detected: "
+ + CircularPathFinder.printCircularPath(currentVisited, current));
+ }
+
+ if (allVisited.contains(current)) {
+ return;
+ }
+
+ currentVisited.add(current);
+ try {
+ Collection<SubmoduleSubscription> subscriptions =
+ superProjectSubscriptionsForSubmoduleBranch(current);
+ for (SubmoduleSubscription sub : subscriptions) {
+ BranchNameKey superBranch = sub.getSuperProject();
+ searchForSuperprojects(superBranch, currentVisited, allVisited);
+ targets.put(superBranch, sub);
+ branchesByProject.put(superBranch.project(), superBranch);
+ affectedBranches.add(superBranch);
+ affectedBranches.add(sub.getSubmodule());
+ subscribedBranches.add(sub.getSubmodule());
+ }
+ } catch (IOException e) {
+ throw new StorageException("Cannot find superprojects for " + current, e);
+ }
+ currentVisited.remove(current);
+ allVisited.add(current);
+ }
+
+ private Collection<BranchNameKey> getDestinationBranches(BranchNameKey src, SubscribeSection s)
+ throws IOException {
+ Collection<BranchNameKey> ret = new HashSet<>();
+ logger.atFine().log("Inspecting SubscribeSection %s", s);
+ for (RefSpec r : s.getMatchingRefSpecs()) {
+ logger.atFine().log("Inspecting [matching] ref %s", r);
+ if (!r.matchSource(src.branch())) {
+ continue;
+ }
+ if (r.isWildcard()) {
+ // refs/heads/*[:refs/somewhere/*]
+ ret.add(
+ BranchNameKey.create(
+ s.getProject(), r.expandFromSource(src.branch()).getDestination()));
+ } else {
+ // e.g. refs/heads/master[:refs/heads/stable]
+ String dest = r.getDestination();
+ if (dest == null) {
+ dest = r.getSource();
+ }
+ ret.add(BranchNameKey.create(s.getProject(), dest));
+ }
+ }
+
+ for (RefSpec r : s.getMultiMatchRefSpecs()) {
+ logger.atFine().log("Inspecting [all] ref %s", r);
+ if (!r.matchSource(src.branch())) {
+ continue;
+ }
+ OpenRepo or;
+ try {
+ or = orm.getRepo(s.getProject());
+ } catch (NoSuchProjectException e) {
+ // A project listed a non existent project to be allowed
+ // to subscribe to it. Allow this for now, i.e. no exception is
+ // thrown.
+ continue;
+ }
+
+ for (Ref ref : or.repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_HEADS)) {
+ if (r.getDestination() != null && !r.matchDestination(ref.getName())) {
+ continue;
+ }
+ BranchNameKey b = BranchNameKey.create(s.getProject(), ref.getName());
+ if (!ret.contains(b)) {
+ ret.add(b);
+ }
+ }
+ }
+ logger.atFine().log("Returning possible branches: %s for project %s", ret, s.getProject());
+ return ret;
+ }
+
+ private Collection<SubmoduleSubscription> superProjectSubscriptionsForSubmoduleBranch(
+ BranchNameKey srcBranch) throws IOException {
+ logger.atFine().log("Calculating possible superprojects for %s", srcBranch);
+ Collection<SubmoduleSubscription> ret = new ArrayList<>();
+ Project.NameKey srcProject = srcBranch.project();
+ for (SubscribeSection s :
+ projectCache
+ .get(srcProject)
+ .orElseThrow(illegalState(srcProject))
+ .getSubscribeSections(srcBranch)) {
+ logger.atFine().log("Checking subscribe section %s", s);
+ Collection<BranchNameKey> branches = getDestinationBranches(srcBranch, s);
+ for (BranchNameKey targetBranch : branches) {
+ Project.NameKey targetProject = targetBranch.project();
+ try {
+ OpenRepo or = orm.getRepo(targetProject);
+ ObjectId id = or.repo.resolve(targetBranch.branch());
+ if (id == null) {
+ logger.atFine().log("The branch %s doesn't exist.", targetBranch);
+ continue;
+ }
+ } catch (NoSuchProjectException e) {
+ logger.atFine().log("The project %s doesn't exist", targetProject);
+ continue;
+ }
+
+ GitModules m = branchGitModules.get(targetBranch);
+ if (m == null) {
+ m = gitmodulesFactory.create(targetBranch, orm);
+ branchGitModules.put(targetBranch, m);
+ }
+ ret.addAll(m.subscribedTo(srcBranch));
+ }
+ }
+ logger.atFine().log("Calculated superprojects for %s are %s", srcBranch, ret);
+ return ret;
+ }
+
+ private static <T> void reverse(LinkedHashSet<T> set) {
+ if (set == null) {
+ return;
+ }
+
+ Deque<T> q = new ArrayDeque<>(set);
+ set.clear();
+
+ while (!q.isEmpty()) {
+ set.add(q.removeLast());
+ }
+ }
+ }
+}
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/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index 8800463..a682d33 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -15,10 +15,11 @@
package com.google.gerrit.testing;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService;
import static com.google.inject.Scopes.SINGLETON;
import com.google.common.base.Strings;
-import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperationsImpl;
import com.google.gerrit.extensions.client.AuthType;
@@ -30,6 +31,7 @@
import com.google.gerrit.index.project.ProjectSchemaDefinitions;
import com.google.gerrit.metrics.DisabledMetricMaker;
import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.server.CacheRefreshExecutor;
import com.google.gerrit.server.FanOutExecutor;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.GerritPersonIdentProvider;
@@ -209,7 +211,7 @@
@Singleton
@DiffExecutor
public ExecutorService createDiffExecutor() {
- return MoreExecutors.newDirectExecutorService();
+ return newDirectExecutorService();
}
});
install(new DefaultMemoryCacheModule());
@@ -277,7 +279,7 @@
@Singleton
@SendEmailExecutor
public ExecutorService createSendEmailExecutor() {
- return MoreExecutors.newDirectExecutorService();
+ return newDirectExecutorService();
}
@Provides
@@ -287,6 +289,13 @@
return queues.createQueue(2, "FanOut");
}
+ @Provides
+ @Singleton
+ @CacheRefreshExecutor
+ public ListeningExecutorService createCacheRefreshExecutor() {
+ return newDirectExecutorService();
+ }
+
private Module luceneIndexModule() {
return indexModule("com.google.gerrit.lucene.LuceneIndexModule");
}
diff --git a/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java b/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
new file mode 100644
index 0000000..8e08b1c
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/OutgoingEmailIT.java
@@ -0,0 +1,138 @@
+// 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.accounts.EmailInput;
+import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+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;
+
+public class OutgoingEmailIT extends AbstractDaemonTest {
+
+ @Test
+ public void messageIdHeaderFromChangeUpdate() throws Exception {
+ Repository repository = repoManager.openRepository(project);
+ PushOneCommit.Result result = createChange();
+ AddReviewerInput addReviewerInput = new AddReviewerInput();
+ addReviewerInput.reviewer = user.email();
+ gApi.changes().id(result.getChangeId()).addReviewer(addReviewerInput);
+ sender.clear();
+
+ gApi.changes().id(result.getChangeId()).abandon();
+ assertThat(getMessageId(sender))
+ .isEqualTo(
+ withPrefixAndSuffixForMessageId(
+ repository
+ .getRefDatabase()
+ .exactRef(result.getChange().getId().toRefPrefix() + "meta")
+ .getObjectId()
+ .getName()
+ + "-HTML"));
+ sender.clear();
+
+ gApi.changes().id(result.getChangeId()).restore();
+ assertThat(getMessageId(sender))
+ .isEqualTo(
+ withPrefixAndSuffixForMessageId(
+ repository
+ .getRefDatabase()
+ .exactRef(result.getChange().getId().toRefPrefix() + "meta")
+ .getObjectId()
+ .getName()
+ + "-HTML"));
+ }
+
+ @Test
+ @GerritConfig(
+ name = "auth.registerEmailPrivateKey",
+ value = "HsOc6l_2lhS9G7sE_RsnS7Z6GJjdRDX14co=")
+ public void messageIdHeaderFromAccountUpdate() throws Exception {
+ Repository allUsersRepo = repoManager.openRepository(allUsers);
+ String email = "new.email@example.com";
+ EmailInput input = new EmailInput();
+ input.email = email;
+ sender.clear();
+ gApi.accounts().self().addEmail(input);
+
+ assertThat(sender.getMessages()).hasSize(1);
+ FakeEmailSender.Message m = sender.getMessages().get(0);
+ assertThat(m.rcpt()).containsExactly(Address.create(email));
+
+ assertThat(getMessageId(sender))
+ .isEqualTo(
+ 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(newPassword).isNotNull();
+ assertThat(getMessageId(sender))
+ .containsMatch("<HTTP_password_change-" + admin.id().toString() + ".*@.*>");
+ }
+
+ @Test
+ public void htmlAndPlainTextSuffixAddedToMessageId() throws Exception {
+ PushOneCommit.Result result = createChange();
+ GeneralPreferencesInfo generalPreferencesInfo = new GeneralPreferencesInfo();
+ generalPreferencesInfo.emailFormat = GeneralPreferencesInfo.EmailFormat.PLAINTEXT;
+ gApi.accounts().id(user.id().get()).setPreferences(generalPreferencesInfo);
+ AddReviewerInput addReviewerInput = new AddReviewerInput();
+ addReviewerInput.reviewer = user.email();
+ gApi.changes().id(result.getChangeId()).addReviewer(addReviewerInput);
+ sender.clear();
+
+ gApi.changes().id(result.getChangeId()).current().review(ReviewInput.approve());
+ assertThat(getMessageId(sender)).contains("-PLAIN");
+ sender.clear();
+
+ generalPreferencesInfo.emailFormat = GeneralPreferencesInfo.EmailFormat.HTML_PLAINTEXT;
+ gApi.accounts().id(user.id().get()).setPreferences(generalPreferencesInfo);
+ sender.clear();
+
+ gApi.changes().id(result.getChangeId()).current().review(ReviewInput.reject());
+ assertThat(getMessageId(sender)).contains("-HTML");
+ }
+
+ private static String getMessageId(FakeEmailSender sender) {
+ return ((EmailHeader.String)
+ (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/AccountManagerIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
index a09284e..b41a2f3 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
@@ -26,6 +26,8 @@
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.client.AccountFieldName;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ServerInitiated;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
@@ -33,12 +35,16 @@
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult;
+import com.google.gerrit.server.account.SetInactiveFlag;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdNotes;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.group.db.GroupsUpdate;
import com.google.gerrit.server.notedb.Sequences;
+import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.inject.Inject;
+import com.google.inject.util.Providers;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.lib.Repository;
@@ -51,6 +57,12 @@
@Inject @ServerInitiated private AccountsUpdate accountsUpdate;
@Inject private ExternalIdNotes.Factory extIdNotesFactory;
+ @Inject private Sequences sequences;
+ @Inject private IdentifiedUser.GenericFactory userFactory;
+ @Inject private SshKeyCache sshKeyCache;
+ @Inject private GroupsUpdate.Factory groupsUpdateFactory;
+ @Inject private SetInactiveFlag setInactiveFlag;
+
@Test
public void authenticateNewAccountWithEmail() throws Exception {
String email = "foo@example.com";
@@ -200,6 +212,31 @@
@Test
public void authenticateWithUsernameAndUpdateDisplayName() throws Exception {
+ authenticateWithUsernameAndUpdateDisplayName(accountManager);
+ }
+
+ @Test
+ public void readOnlyFullNameField_authenticateWithUsernameAndUpdateDisplayName()
+ throws Exception {
+ TestRealm realm = server.getTestInjector().getInstance(TestRealm.class);
+ realm.denyEdit(AccountFieldName.FULL_NAME);
+ authenticateWithUsernameAndUpdateDisplayName(
+ new AccountManager(
+ sequences,
+ cfg,
+ accounts,
+ Providers.of(accountsUpdate),
+ accountCache,
+ realm,
+ userFactory,
+ sshKeyCache,
+ projectCache,
+ externalIds,
+ groupsUpdateFactory,
+ setInactiveFlag));
+ }
+
+ private void authenticateWithUsernameAndUpdateDisplayName(AccountManager am) throws Exception {
String username = "foo";
String email = "foo@example.com";
Account.Id accountId = Account.id(seq.nextAccountId());
@@ -215,7 +252,7 @@
AuthRequest who = AuthRequest.forUser(username);
String newName = "Updated Name";
who.setDisplayName(newName);
- AuthResult authResult = accountManager.authenticate(who);
+ AuthResult authResult = am.authenticate(who);
assertAuthResultForExistingAccount(authResult, accountId, gerritExtIdKey);
Optional<AccountState> accountState = accounts.get(accountId);
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/BUILD b/javatests/com/google/gerrit/acceptance/api/accounts/BUILD
index 2167d27..673379d 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/BUILD
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/BUILD
@@ -1,3 +1,4 @@
+load("@rules_java//java:defs.bzl", "java_library")
load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
acceptance_tests(
@@ -9,8 +10,18 @@
"no_windows",
],
deps = [
+ ":util",
"//java/com/google/gerrit/git",
"//java/com/google/gerrit/mail",
"//java/com/google/gerrit/server/util/time",
],
)
+
+java_library(
+ name = "util",
+ testonly = True,
+ srcs = glob(["TestRealm.java"]),
+ deps = [
+ "//java/com/google/gerrit/acceptance:lib",
+ ],
+)
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java
new file mode 100644
index 0000000..a8fd834
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/MessageIdGeneratorIT.java
@@ -0,0 +1,81 @@
+// 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 com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.mail.MailMessage;
+import com.google.gerrit.server.mail.send.MessageIdGenerator;
+import com.google.gerrit.server.util.time.TimeUtil;
+import java.time.Instant;
+import javax.inject.Inject;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Test;
+
+public class MessageIdGeneratorIT extends AbstractDaemonTest {
+ @Inject private MessageIdGenerator messageIdGenerator;
+
+ @Test
+ public void fromAccountUpdate() throws Exception {
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ String messageId = messageIdGenerator.fromAccountUpdate(admin.id()).id();
+ String sha1 =
+ repo.getRefDatabase().findRef(RefNames.refsUsers(admin.id())).getObjectId().getName();
+ assertThat(sha1).isEqualTo(messageId);
+ }
+ }
+
+ @Test
+ public void fromChangeUpdate() throws Exception {
+ try (Repository repo = repoManager.openRepository(project)) {
+ PushOneCommit.Result result = createChange();
+ PatchSet.Id patchsetId = result.getChange().currentPatchSet().id();
+ String messageId = messageIdGenerator.fromChangeUpdate(project, patchsetId).id();
+ String sha1 =
+ repo.getRefDatabase()
+ .findRef(String.format("%smeta", patchsetId.changeId().toRefPrefix()))
+ .getObjectId()
+ .getName();
+ assertThat(sha1).isEqualTo(messageId);
+ }
+ }
+
+ @Test
+ public void fromMailMessage() throws Exception {
+ String id = "unique-id";
+ MailMessage mailMessage =
+ MailMessage.builder()
+ .id(id)
+ .from(Address.create("email@email.com"))
+ .dateReceived(Instant.EPOCH)
+ .subject("subject")
+ .build();
+ assertThat(messageIdGenerator.fromMailMessage(mailMessage).id()).isEqualTo(id + "-REJECTION");
+ }
+
+ @Test
+ public void fromReasonAccountIdAndTimestamp() throws Exception {
+ String reason = "reason";
+ 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/accounts/TestRealm.java b/javatests/com/google/gerrit/acceptance/api/accounts/TestRealm.java
new file mode 100644
index 0000000..94b6cd3
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/TestRealm.java
@@ -0,0 +1,46 @@
+// 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 com.google.gerrit.extensions.client.AccountFieldName;
+import com.google.gerrit.server.account.DefaultRealm;
+import com.google.gerrit.server.account.EmailExpander;
+import com.google.gerrit.server.account.Emails;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.util.HashSet;
+import java.util.Set;
+
+@Singleton
+public class TestRealm extends DefaultRealm {
+
+ private final Set<AccountFieldName> readOnlyFields = new HashSet<>();
+
+ @Inject
+ public TestRealm(EmailExpander emailExpander, Provider<Emails> emails, AuthConfig authConfig) {
+ super(emailExpander, emails, authConfig);
+ }
+
+ public void denyEdit(AccountFieldName field) {
+ readOnlyFields.add(field);
+ }
+
+ @Override
+ public boolean allowsEdit(AccountFieldName field) {
+ return !readOnlyFields.contains(field);
+ }
+}
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/api/change/PostReviewIT.java b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
index be0cc04..7d73374 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
@@ -31,13 +31,16 @@
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
+import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.RobotCommentInfo;
import com.google.gerrit.extensions.config.FactoryModule;
@@ -54,6 +57,7 @@
import java.sql.Timestamp;
import java.util.Collection;
import java.util.List;
+import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
@@ -64,6 +68,7 @@
@Inject private CommentValidator mockCommentValidator;
@Inject private TestCommentHelper testCommentHelper;
+ @Inject private RequestScopeOperations requestScopeOperations;
private static final String COMMENT_TEXT = "The comment text";
private static final CommentForValidation FILE_COMMENT_FOR_VALIDATION =
@@ -382,6 +387,93 @@
.contains("Exceeding maximum cumulative size of comments");
}
+ @Test
+ public void ccToReviewer() throws Exception {
+ PushOneCommit.Result r = createChange();
+ // User adds themselves and changes state
+ requestScopeOperations.setApiUser(user.id());
+
+ ReviewInput input = new ReviewInput().reviewer(user.id().toString(), ReviewerState.CC, false);
+ gApi.changes().id(r.getChangeId()).current().review(input);
+
+ Map<ReviewerState, Collection<AccountInfo>> reviewers =
+ gApi.changes().id(r.getChangeId()).get().reviewers;
+ assertThat(reviewers).hasSize(1);
+ AccountInfo reviewer = Iterables.getOnlyElement(reviewers.get(ReviewerState.CC));
+ assertThat(reviewer._accountId).isEqualTo(user.id().get());
+
+ // CC -> Reviewer
+ ReviewInput input2 = new ReviewInput().reviewer(user.id().toString());
+ gApi.changes().id(r.getChangeId()).current().review(input2);
+
+ Map<ReviewerState, Collection<AccountInfo>> reviewers2 =
+ gApi.changes().id(r.getChangeId()).get().reviewers;
+ assertThat(reviewers2).hasSize(1);
+ AccountInfo reviewer2 = Iterables.getOnlyElement(reviewers2.get(ReviewerState.REVIEWER));
+ assertThat(reviewer2._accountId).isEqualTo(user.id().get());
+ }
+
+ @Test
+ public void reviewerToCc() throws Exception {
+ // Admin owns the change
+ PushOneCommit.Result r = createChange();
+ // User adds themselves and changes state
+ requestScopeOperations.setApiUser(user.id());
+
+ ReviewInput input = new ReviewInput().reviewer(user.id().toString());
+ gApi.changes().id(r.getChangeId()).current().review(input);
+
+ Map<ReviewerState, Collection<AccountInfo>> reviewers =
+ gApi.changes().id(r.getChangeId()).get().reviewers;
+ assertThat(reviewers).hasSize(1);
+ AccountInfo reviewer = Iterables.getOnlyElement(reviewers.get(ReviewerState.REVIEWER));
+ assertThat(reviewer._accountId).isEqualTo(user.id().get());
+
+ // Reviewer -> CC
+ ReviewInput input2 = new ReviewInput().reviewer(user.id().toString(), ReviewerState.CC, false);
+ gApi.changes().id(r.getChangeId()).current().review(input2);
+
+ Map<ReviewerState, Collection<AccountInfo>> reviewers2 =
+ gApi.changes().id(r.getChangeId()).get().reviewers;
+ assertThat(reviewers2).hasSize(1);
+ AccountInfo reviewer2 = Iterables.getOnlyElement(reviewers2.get(ReviewerState.CC));
+ assertThat(reviewer2._accountId).isEqualTo(user.id().get());
+ }
+
+ @Test
+ public void votingMakesCallerReviewer() throws Exception {
+ // Admin owns the change
+ PushOneCommit.Result r = createChange();
+ // User adds themselves and changes state
+ requestScopeOperations.setApiUser(user.id());
+
+ ReviewInput input = new ReviewInput().label("Code-Review", 1);
+ gApi.changes().id(r.getChangeId()).current().review(input);
+
+ Map<ReviewerState, Collection<AccountInfo>> reviewers =
+ gApi.changes().id(r.getChangeId()).get().reviewers;
+ assertThat(reviewers).hasSize(1);
+ AccountInfo reviewer = Iterables.getOnlyElement(reviewers.get(ReviewerState.REVIEWER));
+ assertThat(reviewer._accountId).isEqualTo(user.id().get());
+ }
+
+ @Test
+ public void commentingMakesUserCC() throws Exception {
+ // Admin owns the change
+ PushOneCommit.Result r = createChange();
+ // User adds themselves and changes state
+ requestScopeOperations.setApiUser(user.id());
+
+ ReviewInput input = new ReviewInput().message("Foo bar!");
+ gApi.changes().id(r.getChangeId()).current().review(input);
+
+ Map<ReviewerState, Collection<AccountInfo>> reviewers =
+ gApi.changes().id(r.getChangeId()).get().reviewers;
+ assertThat(reviewers).hasSize(1);
+ AccountInfo reviewer = Iterables.getOnlyElement(reviewers.get(ReviewerState.CC));
+ assertThat(reviewer._accountId).isEqualTo(user.id().get());
+ }
+
private List<RobotCommentInfo> getRobotComments(String changeId) throws RestApiException {
return gApi.changes().id(changeId).robotComments().values().stream()
.flatMap(Collection::stream)
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 fcf0e6d..0514e03 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -17,7 +17,9 @@
import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.schema.AclUtil.grant;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static com.google.gerrit.truth.ConfigSubject.assertThat;
import static com.google.gerrit.truth.MapSubject.assertThatMap;
@@ -31,6 +33,7 @@
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
@@ -92,7 +95,7 @@
@Test
public void grantRevertPermission() throws Exception {
- String ref = "refs/heads/*";
+ String ref = "refs/*";
String groupId = "global:Registered-Users";
grantRevertPermission.execute(newProjectName);
@@ -108,6 +111,69 @@
}
@Test
+ public void grantRevertPermissionByOnNewRefAndDeletingOnOldRef() throws Exception {
+ String refsHeads = "refs/heads/*";
+ String refsStar = "refs/*";
+ String groupId = "global:Registered-Users";
+ GroupReference registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS);
+
+ try (Repository repo = repoManager.openRepository(newProjectName)) {
+ MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, newProjectName, repo);
+ ProjectConfig projectConfig = projectConfigFactory.read(md);
+ AccessSection heads = projectConfig.getAccessSection(AccessSection.HEADS, true);
+ grant(projectConfig, heads, Permission.REVERT, registeredUsers);
+ md.getCommitBuilder().setAuthor(admin.newIdent());
+ md.getCommitBuilder().setCommitter(admin.newIdent());
+ md.setMessage("Add revert permission for all registered users\n");
+
+ projectConfig.commit(md);
+ }
+ grantRevertPermission.execute(newProjectName);
+
+ ProjectAccessInfo info = pApi().access();
+
+ // Revert permission is removed on refs/heads/*.
+ assertThat(info.local.containsKey(refsHeads)).isTrue();
+ AccessSectionInfo accessSectionInfo = info.local.get(refsHeads);
+ assertThat(accessSectionInfo.permissions.containsKey(Permission.REVERT)).isFalse();
+
+ // new permission is added on refs/* with Registered-Users.
+ assertThat(info.local.containsKey(refsStar)).isTrue();
+ accessSectionInfo = info.local.get(refsStar);
+ assertThat(accessSectionInfo.permissions.containsKey(Permission.REVERT)).isTrue();
+ PermissionInfo permissionInfo = accessSectionInfo.permissions.get(Permission.REVERT);
+ assertThat(permissionInfo.rules.containsKey(groupId)).isTrue();
+ PermissionRuleInfo permissionRuleInfo = permissionInfo.rules.get(groupId);
+ assertThat(permissionRuleInfo.action).isEqualTo(PermissionRuleInfo.Action.ALLOW);
+ }
+
+ @Test
+ public void grantRevertPermissionDoesntDeleteAdminsPreferences() throws Exception {
+ GroupReference registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS);
+ GroupReference otherGroup = systemGroupBackend.getGroup(ANONYMOUS_USERS);
+
+ try (Repository repo = repoManager.openRepository(newProjectName)) {
+ MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, newProjectName, repo);
+ ProjectConfig projectConfig = projectConfigFactory.read(md);
+ AccessSection heads = projectConfig.getAccessSection(AccessSection.HEADS, true);
+ grant(projectConfig, heads, Permission.REVERT, registeredUsers);
+ grant(projectConfig, heads, Permission.REVERT, otherGroup);
+ md.getCommitBuilder().setAuthor(admin.newIdent());
+ md.getCommitBuilder().setCommitter(admin.newIdent());
+ md.setMessage("Add revert permission for all registered users\n");
+
+ projectConfig.commit(md);
+ }
+ ProjectAccessInfo expected = pApi().access();
+
+ grantRevertPermission.execute(newProjectName);
+ projectCache.evict(newProjectName);
+ ProjectAccessInfo actual = pApi().access();
+ // Permissions don't change
+ assertThat(expected.local).isEqualTo(actual.local);
+ }
+
+ @Test
public void grantRevertPermissionOnlyWorksOnce() throws Exception {
grantRevertPermission.execute(newProjectName);
grantRevertPermission.execute(newProjectName);
@@ -115,9 +181,9 @@
try (Repository repo = repoManager.openRepository(newProjectName)) {
MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, newProjectName, repo);
ProjectConfig projectConfig = projectConfigFactory.read(md);
- AccessSection heads = projectConfig.getAccessSection(AccessSection.HEADS, true);
+ AccessSection all = projectConfig.getAccessSection(AccessSection.ALL, true);
- Permission permission = heads.getPermission(Permission.REVERT);
+ Permission permission = all.getPermission(Permission.REVERT);
assertThat(permission.getRules()).hasSize(1);
}
}
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 fc44822..d7d67b8 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -40,6 +40,7 @@
import com.google.gerrit.extensions.validators.CommentForValidation;
import com.google.gerrit.extensions.validators.CommentValidationContext;
import com.google.gerrit.extensions.validators.CommentValidator;
+import com.google.gerrit.mail.EmailHeader;
import com.google.gerrit.mail.MailMessage;
import com.google.gerrit.mail.MailProcessingUtil;
import com.google.gerrit.server.mail.receive.MailProcessor;
@@ -47,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;
@@ -296,6 +298,10 @@
assertNotifyTo(user);
Message message = sender.nextMessage();
assertThat(message.body()).contains("rejected one or more comments");
+
+ // ensure the message header contains a valid message id.
+ assertThat(((EmailHeader.String) (message.headers().get("Message-ID"))).getString())
+ .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/BUILD b/javatests/com/google/gerrit/server/cache/h2/BUILD
index 98f1b0e..438990c 100644
--- a/javatests/com/google/gerrit/server/cache/h2/BUILD
+++ b/javatests/com/google/gerrit/server/cache/h2/BUILD
@@ -6,10 +6,12 @@
deps = [
"//java/com/google/gerrit/server/cache/h2",
"//java/com/google/gerrit/server/cache/serialize",
+ "//java/com/google/gerrit/server/util/time",
"//lib:guava",
"//lib:h2",
"//lib:junit",
"//lib/guice",
+ "//lib/mockito",
"//lib/truth",
],
)
diff --git a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
index 69c2799..ddcfe0c 100644
--- a/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
+++ b/javatests/com/google/gerrit/server/cache/h2/H2CacheTest.java
@@ -16,16 +16,27 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.gerrit.server.cache.h2.H2CacheImpl.SqlStore;
import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
import com.google.gerrit.server.cache.serialize.StringCacheSerializer;
+import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.TypeLiteral;
+import java.time.Duration;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
+import javax.annotation.Nullable;
import org.junit.Test;
public class H2CacheTest {
@@ -38,23 +49,31 @@
}
private static H2CacheImpl<String, String> newH2CacheImpl(
- int id, Cache<String, ValueHolder<String>> mem, int version) {
- SqlStore<String, String> store =
- new SqlStore<>(
- "jdbc:h2:mem:Test_" + id,
- KEY_TYPE,
- StringCacheSerializer.INSTANCE,
- StringCacheSerializer.INSTANCE,
- version,
- 1 << 20,
- null);
+ SqlStore<String, String> store, Cache<String, ValueHolder<String>> mem) {
return new H2CacheImpl<>(MoreExecutors.directExecutor(), store, KEY_TYPE, mem);
}
+ private static SqlStore<String, String> newStore(
+ int id,
+ int version,
+ @Nullable Duration expireAfterWrite,
+ @Nullable Duration refreshAfterWrite) {
+ return new SqlStore<>(
+ "jdbc:h2:mem:Test_" + id,
+ KEY_TYPE,
+ StringCacheSerializer.INSTANCE,
+ StringCacheSerializer.INSTANCE,
+ version,
+ 1 << 20,
+ expireAfterWrite,
+ refreshAfterWrite);
+ }
+
@Test
public void get() throws ExecutionException {
Cache<String, ValueHolder<String>> mem = CacheBuilder.newBuilder().build();
- H2CacheImpl<String, String> impl = newH2CacheImpl(nextDbId(), mem, DEFAULT_VERSION);
+ H2CacheImpl<String, String> impl =
+ newH2CacheImpl(newStore(nextDbId(), DEFAULT_VERSION, null, null), mem);
assertThat(impl.getIfPresent("foo")).isNull();
@@ -94,11 +113,12 @@
}
@Test
- public void version() throws Exception {
+ public void version() {
int id = nextDbId();
- H2CacheImpl<String, String> oldImpl = newH2CacheImpl(id, disableMemCache(), DEFAULT_VERSION);
+ H2CacheImpl<String, String> oldImpl =
+ newH2CacheImpl(newStore(id, DEFAULT_VERSION, null, null), disableMemCache());
H2CacheImpl<String, String> newImpl =
- newH2CacheImpl(id, disableMemCache(), DEFAULT_VERSION + 1);
+ newH2CacheImpl(newStore(id, DEFAULT_VERSION + 1, null, null), disableMemCache());
assertThat(oldImpl.diskStats().space()).isEqualTo(0);
assertThat(newImpl.diskStats().space()).isEqualTo(0);
@@ -124,6 +144,57 @@
assertThat(oldImpl.getIfPresent("key")).isNull();
}
+ @Test
+ public void refreshAfterWrite_triggeredWhenConfigured() throws Exception {
+ SqlStore<String, String> store =
+ newStore(nextDbId(), DEFAULT_VERSION, null, Duration.ofMillis(10));
+
+ // This is the loader that we configure for the cache when calling .loader(...)
+ @SuppressWarnings("unchecked")
+ CacheLoader<String, String> baseLoader = mock(CacheLoader.class);
+ resetLoaderAndAnswerLoadAndRefreshCalls(baseLoader);
+
+ // We wrap baseLoader just like H2CacheFactory is wrapping it. The wrapped version will call out
+ // to the store for refreshing values.
+ H2CacheImpl.Loader<String, String> wrappedLoader =
+ new H2CacheImpl.Loader<>(MoreExecutors.directExecutor(), store, baseLoader);
+ // memCache is the in-memory variant of the cache. Its loader is wrappedLoader which will call
+ // out to the store to save or delete cached values.
+ LoadingCache<String, ValueHolder<String>> memCache =
+ CacheBuilder.newBuilder().maximumSize(10).build(wrappedLoader);
+
+ // h2Cache puts it all together
+ H2CacheImpl<String, String> h2Cache = newH2CacheImpl(store, memCache);
+
+ // Initial load and cache retrieval do not trigger refresh
+ // This works because we use a directExecutor() for refreshes
+ TimeUtil.setCurrentMillisSupplier(() -> 0);
+ assertThat(h2Cache.get("foo")).isEqualTo("load:foo");
+ verify(baseLoader).load("foo");
+ assertThat(h2Cache.get("foo")).isEqualTo("load:foo");
+ verifyNoMoreInteractions(baseLoader);
+ resetLoaderAndAnswerLoadAndRefreshCalls(baseLoader);
+
+ // Load after refresh duration returns old value, triggers refresh and returns new value
+ TimeUtil.setCurrentMillisSupplier(() -> 11);
+ assertThat(h2Cache.get("foo")).isEqualTo("load:foo");
+ assertThat(h2Cache.get("foo")).isEqualTo("reload:foo");
+ verify(baseLoader).reload("foo", "load:foo");
+ verifyNoMoreInteractions(baseLoader);
+ resetLoaderAndAnswerLoadAndRefreshCalls(baseLoader);
+
+ // Refreshed value was persisted
+ memCache.invalidateAll(); // Invalidates only the memcache, not the store.
+ assertThat(h2Cache.getIfPresent("foo")).isEqualTo("reload:foo");
+ }
+
+ private static void resetLoaderAndAnswerLoadAndRefreshCalls(CacheLoader<String, String> loader)
+ throws Exception {
+ reset(loader);
+ when(loader.load("foo")).thenReturn("load:foo");
+ when(loader.reload("foo", "load:foo")).thenReturn(Futures.immediateFuture("reload:foo"));
+ }
+
private static <K, V> Cache<K, ValueHolder<V>> disableMemCache() {
return CacheBuilder.newBuilder().maximumSize(0).build();
}
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/javatests/com/google/gerrit/server/submit/SubscriptionGraphTest.java b/javatests/com/google/gerrit/server/submit/SubscriptionGraphTest.java
new file mode 100644
index 0000000..dbcc209
--- /dev/null
+++ b/javatests/com/google/gerrit/server/submit/SubscriptionGraphTest.java
@@ -0,0 +1,214 @@
+// 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.submit;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.data.SubscribeSection;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.SubmoduleSubscription;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.submit.SubscriptionGraph.DefaultFactory;
+import com.google.gerrit.testing.InMemoryRepositoryManager;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+public class SubscriptionGraphTest {
+ private static final String TEST_PATH = "test/path";
+ private static final Project.NameKey SUPER_PROJECT = Project.nameKey("Superproject");
+ private static final Project.NameKey SUB_PROJECT = Project.nameKey("Subproject");
+ private static final BranchNameKey SUPER_BRANCH =
+ BranchNameKey.create(SUPER_PROJECT, "refs/heads/one");
+ private static final BranchNameKey SUB_BRANCH =
+ BranchNameKey.create(SUB_PROJECT, "refs/heads/one");
+ private InMemoryRepositoryManager repoManager = new InMemoryRepositoryManager();
+ private MergeOpRepoManager mergeOpRepoManager;
+
+ @Mock GitModules.Factory mockGitModulesFactory = mock(GitModules.Factory.class);
+ @Mock ProjectCache mockProjectCache = mock(ProjectCache.class);
+ @Mock ProjectState mockProjectState = mock(ProjectState.class);
+
+ @Before
+ public void setUp() throws Exception {
+ when(mockProjectCache.get(any())).thenReturn(Optional.of(mockProjectState));
+ mergeOpRepoManager = new MergeOpRepoManager(repoManager, mockProjectCache, null, null);
+
+ GitModules emptyMockGitModules = mock(GitModules.class);
+ when(emptyMockGitModules.subscribedTo(any())).thenReturn(ImmutableSet.of());
+ when(mockGitModulesFactory.create(any(), any())).thenReturn(emptyMockGitModules);
+
+ TestRepository<Repository> superProject = createRepo(SUPER_PROJECT);
+ TestRepository<Repository> submoduleProject = createRepo(SUB_PROJECT);
+
+ // Make sure that SUPER_BRANCH and SUB_BRANCH can be subscribed.
+ allowSubscription(SUPER_BRANCH);
+ allowSubscription(SUB_BRANCH);
+
+ setSubscription(SUB_BRANCH, ImmutableList.of(SUPER_BRANCH));
+ setSubscription(SUPER_BRANCH, ImmutableList.of());
+ createBranch(
+ superProject, SUPER_BRANCH, superProject.commit().message("Initial commit").create());
+ createBranch(
+ submoduleProject, SUB_BRANCH, submoduleProject.commit().message("Initial commit").create());
+ }
+
+ @Test
+ public void oneSuperprojectOneSubmodule() throws Exception {
+ SubscriptionGraph.Factory factory =
+ new DefaultFactory(mockGitModulesFactory, mockProjectCache, mergeOpRepoManager);
+ SubscriptionGraph subscriptionGraph = factory.compute(ImmutableSet.of(SUB_BRANCH));
+
+ assertThat(subscriptionGraph.getAffectedSuperProjects()).containsExactly(SUPER_PROJECT);
+ assertThat(subscriptionGraph.getAffectedSuperBranches(SUPER_PROJECT))
+ .containsExactly(SUPER_BRANCH);
+ assertThat(subscriptionGraph.getSubscriptions(SUPER_BRANCH))
+ .containsExactly(new SubmoduleSubscription(SUPER_BRANCH, SUB_BRANCH, TEST_PATH));
+ assertThat(subscriptionGraph.hasSuperproject(SUB_BRANCH)).isTrue();
+ assertThat(subscriptionGraph.getSortedSuperprojectAndSubmoduleBranches())
+ .containsExactly(SUB_BRANCH, SUPER_BRANCH)
+ .inOrder();
+ }
+
+ @Test
+ public void circularSubscription() throws Exception {
+ SubscriptionGraph.Factory factory =
+ new DefaultFactory(mockGitModulesFactory, mockProjectCache, mergeOpRepoManager);
+ setSubscription(SUPER_BRANCH, ImmutableList.of(SUB_BRANCH));
+ SubmoduleConflictException e =
+ assertThrows(
+ SubmoduleConflictException.class, () -> factory.compute(ImmutableSet.of(SUB_BRANCH)));
+
+ String expectedErrorMessage =
+ "Subproject,refs/heads/one->Superproject,refs/heads/one->Subproject,refs/heads/one";
+ assertThat(e).hasMessageThat().contains(expectedErrorMessage);
+ }
+
+ @Test
+ public void multipleSuperprojectsToMultipleSubmodules() throws Exception {
+ // Create superprojects and subprojects.
+ Project.NameKey superProject1 = Project.nameKey("superproject1");
+ Project.NameKey superProject2 = Project.nameKey("superproject2");
+ Project.NameKey subProject1 = Project.nameKey("subproject1");
+ Project.NameKey subProject2 = Project.nameKey("subproject2");
+ TestRepository<Repository> superProjectRepo1 = createRepo(superProject1);
+ TestRepository<Repository> superProjectRepo2 = createRepo(superProject2);
+ TestRepository<Repository> submoduleRepo1 = createRepo(subProject1);
+ TestRepository<Repository> submoduleRepo2 = createRepo(subProject2);
+
+ // Initialize super branches.
+ BranchNameKey superBranch1 = BranchNameKey.create(superProject1, "refs/heads/one");
+ BranchNameKey superBranch2 = BranchNameKey.create(superProject2, "refs/heads/one");
+ createBranch(
+ superProjectRepo1,
+ superBranch1,
+ superProjectRepo1.commit().message("Initial commit").create());
+ createBranch(
+ superProjectRepo2,
+ superBranch2,
+ superProjectRepo2.commit().message("Initial commit").create());
+
+ // Initialize sub branches.
+ BranchNameKey submoduleBranch1 = BranchNameKey.create(subProject1, "refs/heads/one");
+ BranchNameKey submoduleBranch2 = BranchNameKey.create(subProject1, "refs/heads/two");
+ BranchNameKey submoduleBranch3 = BranchNameKey.create(subProject2, "refs/heads/one");
+ createBranch(
+ submoduleRepo1, submoduleBranch1, submoduleRepo1.commit().message("Commit1").create());
+ createBranch(
+ submoduleRepo1, submoduleBranch2, submoduleRepo1.commit().message("Commit2").create());
+ createBranch(
+ submoduleRepo2, submoduleBranch3, submoduleRepo2.commit().message("Commit1").create());
+
+ allowSubscription(submoduleBranch1);
+ allowSubscription(submoduleBranch2);
+ allowSubscription(submoduleBranch3);
+
+ // Initialize subscriptions.
+ setSubscription(submoduleBranch1, ImmutableList.of(superBranch1, superBranch2));
+ setSubscription(submoduleBranch2, ImmutableList.of(superBranch1));
+ setSubscription(submoduleBranch3, ImmutableList.of(superBranch1, superBranch2));
+
+ SubscriptionGraph.Factory factory =
+ new DefaultFactory(mockGitModulesFactory, mockProjectCache, mergeOpRepoManager);
+ SubscriptionGraph subscriptionGraph =
+ factory.compute(ImmutableSet.of(submoduleBranch1, submoduleBranch2));
+
+ assertThat(subscriptionGraph.getAffectedSuperProjects())
+ .containsExactly(superProject1, superProject2);
+ assertThat(subscriptionGraph.getAffectedSuperBranches(superProject1))
+ .containsExactly(superBranch1);
+ assertThat(subscriptionGraph.getAffectedSuperBranches(superProject2))
+ .containsExactly(superBranch2);
+
+ assertThat(subscriptionGraph.getSubscriptions(superBranch1))
+ .containsExactly(
+ new SubmoduleSubscription(superBranch1, submoduleBranch1, TEST_PATH),
+ new SubmoduleSubscription(superBranch1, submoduleBranch2, TEST_PATH));
+ assertThat(subscriptionGraph.getSubscriptions(superBranch2))
+ .containsExactly(new SubmoduleSubscription(superBranch2, submoduleBranch1, TEST_PATH));
+
+ assertThat(subscriptionGraph.hasSuperproject(submoduleBranch1)).isTrue();
+ assertThat(subscriptionGraph.hasSuperproject(submoduleBranch2)).isTrue();
+ assertThat(subscriptionGraph.hasSuperproject(submoduleBranch3)).isFalse();
+
+ assertThat(subscriptionGraph.getSortedSuperprojectAndSubmoduleBranches())
+ .containsExactly(submoduleBranch2, submoduleBranch1, superBranch2, superBranch1)
+ .inOrder();
+ }
+
+ private TestRepository<Repository> createRepo(Project.NameKey project) throws Exception {
+ Repository repo = repoManager.createRepository(project);
+ return new TestRepository<>(repo);
+ }
+
+ private void createBranch(TestRepository<Repository> repo, BranchNameKey branch, RevCommit commit)
+ throws Exception {
+ repo.update(branch.branch(), commit);
+ }
+
+ private void allowSubscription(BranchNameKey branch) {
+ SubscribeSection s = new SubscribeSection(branch.project());
+ s.addMultiMatchRefSpec("refs/heads/*:refs/heads/*");
+ when(mockProjectState.getSubscribeSections(branch)).thenReturn(ImmutableSet.of(s));
+ }
+
+ private void setSubscription(
+ BranchNameKey submoduleBranch, List<BranchNameKey> superprojectBranches) {
+ List<SubmoduleSubscription> subscriptions =
+ superprojectBranches.stream()
+ .map(
+ (targetBranch) ->
+ new SubmoduleSubscription(targetBranch, submoduleBranch, TEST_PATH))
+ .collect(Collectors.toList());
+ GitModules mockGitModules = mock(GitModules.class);
+ when(mockGitModules.subscribedTo(submoduleBranch)).thenReturn(subscriptions);
+ when(mockGitModulesFactory.create(submoduleBranch, mergeOpRepoManager))
+ .thenReturn(mockGitModules);
+ }
+}
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/package.json b/package.json
index 926de4e..0f10b62 100644
--- a/package.json
+++ b/package.json
@@ -4,8 +4,8 @@
"description": "Gerrit Code Review",
"dependencies": {},
"devDependencies": {
- "@bazel/rollup": "^1.1.0",
- "@bazel/typescript": "^1.0.1",
+ "@bazel/rollup": "^1.6.1",
+ "@bazel/typescript": "^1.6.1",
"eslint": "^6.6.0",
"eslint-config-google": "^0.13.0",
"eslint-plugin-html": "^6.0.0",
@@ -15,7 +15,7 @@
"fried-twinkie": "^0.2.2",
"polymer-cli": "^1.9.11",
"prettier": "2.0.5",
- "typescript": "^3.7.4",
+ "typescript": "3.8.2",
"web-component-tester": "^6.5.1"
},
"scripts": {
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 e345e6e..7671def 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit e345e6e79900a72981e4ad19d37c7fbdcae4818b
+Subproject commit 7671def07882aab89b19eb7496418588ea7375d9
diff --git a/plugins/replication b/plugins/replication
index 9ae2087..5838489 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 9ae20871646a0757979e3bef03aaf4e74af8ff73
+Subproject commit 583848945b150503fb76fb780f53bc5c2b42546c
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 9e7fd9b..e952b92 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 9e7fd9b420ac9a5caa045cf82b566cc0b51c93ad
+Subproject commit e952b920ecbee5225f1098a02d4a39b19aa7e234
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-change-list-item/gr-change-list-item_html.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js
index f29c8bf..4e826aa 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js
@@ -83,10 +83,10 @@
line-height: var(--line-height-mono);
}
.u-green {
- color: var(--vote-text-color-recommended);
+ color: var(--positive-green-text-color);
}
.u-red {
- color: var(--vote-text-color-disliked);
+ color: var(--negative-red-text-color);
}
.u-gray-background {
background-color: var(--table-header-background-color);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.js
index 4add1da..b322f38 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.js
@@ -86,14 +86,15 @@
href$="[[_computeNavLink(_query, _offset, -1, _changesPerPage)]]"
class$="[[_computePrevArrowClass(_offset)]]"
>
- <iron-icon icon="gr-icons:chevron-left"></iron-icon>
+ <iron-icon icon="gr-icons:chevron-left" aria-label="Older"> </iron-icon>
</a>
<a
id="nextArrow"
href$="[[_computeNavLink(_query, _offset, 1, _changesPerPage)]]"
class$="[[_computeNextArrowClass(_changes)]]"
>
- <iron-icon icon="gr-icons:chevron-right"></iron-icon>
+ <iron-icon icon="gr-icons:chevron-right" aria-label="Newer">
+ </iron-icon>
</a>
</nav>
</div>
diff --git a/polygerrit-ui/app/elements/change-list/gr-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-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 03a64a5..8dc8058 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -87,12 +87,15 @@
PRIVATE: 'private',
PRIVATE_DELETE: 'private.delete',
PUBLISH_EDIT: 'publishEdit',
+ REBASE: 'rebase',
REBASE_EDIT: 'rebaseEdit',
+ READY: 'ready',
RESTORE: 'restore',
REVERT: 'revert',
REVERT_SUBMISSION: 'revert_submission',
REVIEWED: 'reviewed',
STOP_EDIT: 'stopEdit',
+ SUBMIT: 'submit',
UNIGNORE: 'unignore',
UNREVIEWED: 'unreviewed',
WIP: 'wip',
@@ -205,6 +208,7 @@
ChangeActions.DELETE_EDIT,
ChangeActions.EDIT,
ChangeActions.PUBLISH_EDIT,
+ ChangeActions.READY,
ChangeActions.REBASE_EDIT,
ChangeActions.RESTORE,
ChangeActions.REVERT,
@@ -293,6 +297,7 @@
type: Array,
value() {
return [
+ ChangeActions.READY,
RevisionActions.SUBMIT,
];
},
@@ -1353,6 +1358,8 @@
case ChangeActions.DELETE_EDIT:
case ChangeActions.PUBLISH_EDIT:
case ChangeActions.REBASE_EDIT:
+ case ChangeActions.REBASE:
+ case ChangeActions.SUBMIT:
GerritNav.navigateToChange(this.change);
break;
case ChangeActions.REVERT_SUBMISSION:
@@ -1539,6 +1546,12 @@
if (ACTIONS_WITH_ICONS.has(action.__key)) {
action.icon = action.__key;
}
+ // TODO(brohlfs): Temporary hack until change 269573 is live in all
+ // backends.
+ if (action.__key === ChangeActions.READY) {
+ action.label = 'Mark as Active';
+ }
+ // End of hack
return action;
})
.filter(action => !this._shouldSkipAction(action));
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js
index 916a56f..200ffc9 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js
@@ -137,7 +137,6 @@
<gr-dropdown
id="moreActions"
link=""
- tabindex="0"
vertical-offset="32"
horizontal-align="right"
on-tap-item="_handleOverflowItemTap"
@@ -145,7 +144,8 @@
disabled-ids="[[_disabledMenuActions]]"
items="[[_menuActions]]"
>
- <iron-icon icon="gr-icons:more-vert"></iron-icon>
+ <iron-icon icon="gr-icons:more-vert" aria-labelledby="moreMessage">
+ </iron-icon>
<span id="moreMessage">More</span>
</gr-dropdown>
</div>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index a763250..cda53c5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -415,6 +415,17 @@
});
});
+ test('rebase change calls navigateToChange', done => {
+ const navigateToChangeStub = sandbox.stub(GerritNav, 'navigateToChange');
+ sandbox.stub(element.$.restAPI, 'getResponseObject').returns(
+ Promise.resolve({}));
+ element._handleResponse({__key: 'rebase'}, {});
+ flush(() => {
+ assert.isTrue(navigateToChangeStub.called);
+ done();
+ });
+ });
+
test(`rebase dialog gets recent changes each time it's opened`, done => {
const fetchChangesStub = sandbox.stub(element.$.confirmRebase,
'fetchRecentChanges').returns(Promise.resolve([]));
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js
index 1b18412..ca176be 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js
@@ -74,10 +74,10 @@
color: #ffa62f;
}
.icon.invalid {
- color: var(--vote-text-color-disliked);
+ color: var(--negative-red-text-color);
}
.icon.trusted {
- color: var(--vote-text-color-recommended);
+ color: var(--positive-green-text-color);
}
.parentList.notCurrent.nonMerge #parentNotCurrentMessage {
--arrow-color: #ffa62f;
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_html.js b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.js
index 0da31de..657915b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.js
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.js
@@ -31,10 +31,10 @@
line-height: var(--line-height-mono);
}
.approved.status {
- color: var(--vote-text-color-recommended);
+ color: var(--positive-green-text-color);
}
.rejected.status {
- color: var(--vote-text-color-disliked);
+ color: var(--negative-red-text-color);
}
iron-icon {
color: inherit;
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 05a3ae5..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';
@@ -228,7 +228,6 @@
type: Boolean,
computed: '_computeCanStartReview(_change)',
},
- _comments: Object,
/** @type {?} */
_change: {
type: Object,
@@ -309,7 +308,7 @@
_replyButtonLabel: {
type: String,
value: 'Reply',
- computed: '_computeReplyButtonLabel(_diffDrafts.*)',
+ computed: '_computeReplyButtonLabel(_diffDrafts.*, _canStartReview)',
},
_selectedPatchSet: String,
_shownFileCount: Number,
@@ -921,23 +920,6 @@
this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY);
}
- _handleReadyTap(e) {
- e.preventDefault();
- const button = e && e.target;
- if (button) {
- button.loading = true;
- }
- return this.$.restAPI.startReview(this._changeNum)
- .then(result => {
- this._reload(result);
- })
- .finally(() => {
- if (button) {
- button.loading = false;
- }
- });
- }
-
_handleOpenDiffPrefs() {
this.$.fileList.openDiffPrefs();
}
@@ -1051,10 +1033,7 @@
(value.patchNum !== undefined && value.basePatchNum !== undefined) &&
(this._patchRange.patchNum !== value.patchNum ||
this._patchRange.basePatchNum !== value.basePatchNum);
-
- if (this._changeNum !== value.changeNum) {
- this._initialLoadComplete = false;
- }
+ const changeChanged = this._changeNum !== value.changeNum;
const patchRange = {
patchNum: value.patchNum,
@@ -1066,7 +1045,7 @@
// If the change has already been loaded and the parameter change is only
// in the patch range, then don't do a full reload.
- if (this._initialLoadComplete && patchChanged) {
+ if (!changeChanged && patchChanged) {
if (patchRange.patchNum == null) {
patchRange.patchNum = this.computeLatestPatchNum(this._allPatchSets);
}
@@ -1076,6 +1055,7 @@
return;
}
+ this._initialLoadComplete = false;
this._changeNum = value.changeNum;
this.$.relatedChanges.clear();
@@ -1370,11 +1350,14 @@
return result;
}
- _computeReplyButtonLabel(changeRecord) {
+ _computeReplyButtonLabel(changeRecord, canStartReview) {
// Polymer 2: check for undefined
- if ([changeRecord].some(arg => arg === undefined)) {
+ if ([changeRecord, canStartReview].some(arg => arg === undefined)) {
return 'Reply';
}
+ if (canStartReview) {
+ return 'Start Review';
+ }
const drafts = (changeRecord && changeRecord.base) || {};
const draftCount = Object.keys(drafts)
@@ -1515,15 +1498,8 @@
});
}
- _handleReloadChange(e) {
- return this._reload().then(() => {
- // If the change was rebased or submitted, we need to reload the page
- // with the latest patch.
- const action = e.detail.action;
- if (action === 'rebase' || action === 'submit') {
- GerritNav.navigateToChange(this._change);
- }
- });
+ _handleReloadChange() {
+ return this._reload();
}
_handleGetChangeDetailError(response) {
@@ -1712,6 +1688,13 @@
* (comments, robot comments, draft comments) is requested.
*/
_reloadComments() {
+ // We are resetting all comment related properties, because we want to avoid
+ // a new change being loaded and then paired with outdated comments.
+ this._changeComments = undefined;
+ this._commentThreads = undefined;
+ this._diffDrafts = undefined;
+ this._draftCommentThreads = undefined;
+ this._robotCommentThreads = undefined;
return this.$.commentAPI.loadAll(this._changeNum)
.then(comments => this._recomputeComments(comments));
}
@@ -2020,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
@@ -2129,7 +2112,8 @@
_handleFileActionTap(e) {
e.preventDefault();
- const controls = this.$.fileListHeader.$.editControls;
+ const controls = this.$.fileListHeader
+ .shadowRoot.querySelector('#editControls');
const path = e.detail.path;
switch (e.detail.action) {
case GrEditConstants.Actions.DELETE.id:
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 7548017..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
@@ -172,9 +172,6 @@
margin: var(--spacing-l) 0;
padding: 0 var(--spacing-l);
}
- #startReviewBtn {
- margin-left: var(--spacing-s);
- }
.collapseToggleContainer {
display: flex;
margin-bottom: 8px;
@@ -446,20 +443,11 @@
title="[[createTitle(Shortcut.OPEN_REPLY_DIALOG,
ShortcutSection.ACTIONS)]]"
hidden$="[[!_loggedIn]]"
- primary$="[[!_canStartReview]]"
+ primary=""
disabled="[[_replyDisabled]]"
on-click="_handleReplyTap"
>[[_replyButtonLabel]]</gr-button
>
- <gr-button
- id="startReviewBtn"
- class="startReview"
- title="Switches change state from 'Work in Progress' to 'Active'."
- hidden="[[!_canStartReview]]"
- primary$="[[_canStartReview]]"
- on-click="_handleReadyTap"
- >Start review</gr-button
- >
</div>
<div id="commitMessage" class="commitMessage">
<gr-editable-content
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 6667ddb..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,7 @@
});
const getCustomCssValue =
- cssParam => util.getComputedStyleValue(cssParam, element);
+ cssParam => getComputedStyleValue(cssParam, element);
test('_handleMessageAnchorTap', () => {
element._changeNum = '1';
@@ -1037,7 +1037,7 @@
const getLabel = element._computeReplyButtonLabel;
assert.equal(getLabel(null, false), 'Reply');
- assert.equal(getLabel(null, true), 'Reply');
+ assert.equal(getLabel(null, true), 'Start Review');
const changeRecord = {base: null};
assert.equal(getLabel(changeRecord, false), 'Reply');
@@ -1052,10 +1052,6 @@
assert.equal(getLabel(changeRecord, false), 'Reply (3)');
});
- test('start review button when owner of WIP change', () => {
- assert.equal(element.$.startReviewBtn.innerHTML, 'Start review');
- });
-
test('comment events properly update diff drafts', () => {
element._patchRange = {
basePatchNum: 'PARENT',
@@ -1218,20 +1214,6 @@
assert.isTrue(collapseStub.calledTwice);
});
- test('related changes are updated and new patch selected after rebase',
- done => {
- element._changeNum = '42';
- sandbox.stub(element, 'computeLatestPatchNum', () => 1);
- sandbox.stub(element, '_reload',
- () => Promise.resolve());
- const e = {detail: {action: 'rebase'}};
- element._handleReloadChange(e).then(() => {
- assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
- element._change));
- done();
- });
- });
-
test('related changes are not updated after other action', done => {
sandbox.stub(element, '_reload', () => Promise.resolve());
sandbox.stub(element.$.relatedChanges, 'reload');
@@ -2000,7 +1982,10 @@
};
const fileList = element.$.fileList;
const Actions = GrEditConstants.Actions;
- const controls = element.$.fileListHeader.$.editControls;
+ element.$.fileListHeader.editMode = true;
+ flushAsynchronousOperations();
+ const controls = element.$.fileListHeader
+ .shadowRoot.querySelector('#editControls');
sandbox.stub(controls, 'openDeleteDialog');
sandbox.stub(controls, 'openRenameDialog');
sandbox.stub(controls, 'openRestoreDialog');
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-header/gr-file-list-header_html.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.js
index 10b8606..bb04114 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.js
@@ -188,14 +188,16 @@
</div>
</div>
<div class$="rightControls [[_computeExpandedClass(filesExpanded)]]">
- <span class="showOnEdit flexContainer">
- <gr-edit-controls
- id="editControls"
- patch-num="[[patchNum]]"
- change="[[change]]"
- ></gr-edit-controls>
- <span class="separator"></span>
- </span>
+ <template is="dom-if" if="[[editMode]]">
+ <span class="showOnEdit flexContainer">
+ <gr-edit-controls
+ id="editControls"
+ patch-num="[[patchNum]]"
+ change="[[change]]"
+ ></gr-edit-controls>
+ <span class="separator"></span>
+ </span>
+ </template>
<span class$="[[_computeUploadHelpContainerClass(change, account)]]">
<gr-button link="" class="upload" on-click="_handleUploadTap"
>Update Change</gr-button
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
index 19362d5..2a5a302 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
@@ -299,13 +299,22 @@
});
test('edit-controls visibility', () => {
+ element.editMode = false;
+ flushAsynchronousOperations();
+ // on the first render, when editMode is false, editControls are not
+ // in the DOM to reduce size of DOM and make first render faster.
+ assert.isNull(element.shadowRoot
+ .querySelector('#editControls'));
+
element.editMode = true;
flushAsynchronousOperations();
- assert.isTrue(isVisible(element.$.editControls.parentElement));
+ assert.isTrue(isVisible(element.shadowRoot
+ .querySelector('#editControls').parentElement));
element.editMode = false;
flushAsynchronousOperations();
- assert.isFalse(isVisible(element.$.editControls.parentElement));
+ assert.isFalse(isVisible(element.shadowRoot
+ .querySelector('#editControls').parentElement));
});
test('_computeUploadHelpContainerClass', () => {
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 12b5aad..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
*
@@ -225,6 +227,11 @@
computed: '_computeShowDynamicColumns(_dynamicHeaderEndpoints, ' +
'_dynamicContentEndpoints, _dynamicSummaryEndpoints)',
},
+ _showPrependedDynamicColumns: {
+ type: Boolean,
+ computed: '_computeShowPrependedDynamicColumns(' +
+ '_dynamicPrependedHeaderEndpoints, _dynamicPrependedContentEndpoints)',
+ },
/** @type {Array<string>} */
_dynamicHeaderEndpoints: {
type: Array,
@@ -237,6 +244,14 @@
_dynamicSummaryEndpoints: {
type: Array,
},
+ /** @type {Array<string>} */
+ _dynamicPrependedHeaderEndpoints: {
+ type: Array,
+ },
+ /** @type {Array<string>} */
+ _dynamicPrependedContentEndpoints: {
+ type: Array,
+ },
};
}
@@ -260,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',
@@ -295,18 +312,27 @@
attached() {
super.attached();
pluginLoader.awaitPluginsLoaded().then(() => {
- this._dynamicHeaderEndpoints = pluginEndpoints.getDynamicEndpoints(
- 'change-view-file-list-header');
- this._dynamicContentEndpoints = pluginEndpoints.getDynamicEndpoints(
- 'change-view-file-list-content');
- this._dynamicSummaryEndpoints = pluginEndpoints.getDynamicEndpoints(
- 'change-view-file-list-summary');
+ this._dynamicHeaderEndpoints = pluginEndpoints
+ .getDynamicEndpoints('change-view-file-list-header');
+ this._dynamicContentEndpoints = pluginEndpoints
+ .getDynamicEndpoints('change-view-file-list-content');
+ this._dynamicPrependedHeaderEndpoints = pluginEndpoints
+ .getDynamicEndpoints('change-view-file-list-header-prepend');
+ this._dynamicPrependedContentEndpoints = pluginEndpoints
+ .getDynamicEndpoints('change-view-file-list-content-prepend');
+ this._dynamicSummaryEndpoints = pluginEndpoints
+ .getDynamicEndpoints('change-view-file-list-summary');
if (this._dynamicHeaderEndpoints.length !==
this._dynamicContentEndpoints.length) {
console.warn(
'Different number of dynamic file-list header and content.');
}
+ if (this._dynamicPrependedHeaderEndpoints.length !==
+ this._dynamicPrependedContentEndpoints.length) {
+ console.warn(
+ 'Different number of dynamic file-list header and content.');
+ }
if (this._dynamicHeaderEndpoints.length !==
this._dynamicSummaryEndpoints.length) {
console.warn(
@@ -490,6 +516,9 @@
* @return {string}
*/
_computeCommentsString(changeComments, patchRange, path) {
+ if ([changeComments, patchRange, path].some(arg => arg === undefined)) {
+ return '';
+ }
const unresolvedCount =
changeComments.computeUnresolvedNum({
patchNum: patchRange.basePatchNum,
@@ -529,6 +558,9 @@
* @return {string}
*/
_computeDraftsString(changeComments, patchRange, path) {
+ if ([changeComments, patchRange, path].some(arg => arg === undefined)) {
+ return '';
+ }
const draftCount =
changeComments.computeDraftCount({
patchNum: patchRange.basePatchNum,
@@ -550,6 +582,9 @@
* @return {string}
*/
_computeDraftsStringMobile(changeComments, patchRange, path) {
+ if ([changeComments, patchRange, path].some(arg => arg === undefined)) {
+ return '';
+ }
const draftCount =
changeComments.computeDraftCount({
patchNum: patchRange.basePatchNum,
@@ -571,6 +606,9 @@
* @return {string}
*/
_computeCommentsStringMobile(changeComments, patchRange, path) {
+ if ([changeComments, patchRange, path].some(arg => arg === undefined)) {
+ return '';
+ }
const commentCount =
changeComments.computeCommentCount({
patchNum: patchRange.basePatchNum,
@@ -642,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.
@@ -674,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.
*
@@ -727,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;
@@ -1032,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);
}
}
@@ -1095,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;
@@ -1144,7 +1255,7 @@
}
this._updateDiffCursor();
- this.$.diffCursor.handleDiffUpdate();
+ this.$.diffCursor.reInitAndUpdateStops();
}
_clearCollapsedDiffs(collapsedDiffs) {
@@ -1168,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},
@@ -1205,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();
}));
@@ -1432,11 +1555,23 @@
_computeShowDynamicColumns(
headerEndpoints, contentEndpoints, summaryEndpoints) {
return headerEndpoints && contentEndpoints && summaryEndpoints &&
+ headerEndpoints.length &&
headerEndpoints.length === contentEndpoints.length &&
headerEndpoints.length === summaryEndpoints.length;
}
/**
+ * Shows registered dynamic prepended columns iff the 'header', 'content'
+ * endpoints are registered the exact same number of times.
+ */
+ _computeShowPrependedDynamicColumns(
+ headerEndpoints, contentEndpoints) {
+ return headerEndpoints && contentEndpoints &&
+ headerEndpoints.length &&
+ headerEndpoints.length === contentEndpoints.length;
+ }
+
+ /**
* Returns true if none of the inline diffs have been expanded.
*
* @return {boolean}
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 600b0bc..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;
}
@@ -75,6 +89,7 @@
font-size: var(--font-size-small);
background-color: var(--dark-add-highlight-color);
}
+ .status.invisible,
.status.M {
display: none;
}
@@ -151,10 +166,10 @@
min-width: 3.5em;
}
.added {
- color: var(--vote-text-color-recommended);
+ color: var(--positive-green-text-color);
}
.removed {
- color: var(--vote-text-color-disliked);
+ color: var(--negative-red-text-color);
text-align: left;
min-width: 4em;
padding-left: var(--spacing-s);
@@ -163,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;
@@ -199,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);
@@ -232,6 +247,9 @@
.editFileControls {
width: 7em;
}
+ .markReviewed:focus {
+ outline: none;
+ }
.markReviewed,
.pathLink {
display: inline-block;
@@ -254,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;
}
@@ -294,27 +310,57 @@
display: none;
}
}
+ :host(.hideComments) {
+ --gr-comment-thread-display: none;
+ }
</style>
- <div id="container" on-click="_handleFileListClick">
- <div class="header-row row">
- <div class="path">File</div>
- <div class="comments">Comments</div>
- <div class="sizeBars">Size</div>
- <div class="header-stats">Delta</div>
+ <div
+ id="container"
+ on-click="_handleFileListClick"
+ role="grid"
+ aria-label="Files list"
+ >
+ <div class="header-row row" role="row">
+ <!-- endpoint: change-view-file-list-header-prepend -->
+ <template is="dom-if" if="[[_showPrependedDynamicColumns]]">
+ <template
+ is="dom-repeat"
+ items="[[_dynamicPrependedHeaderEndpoints]]"
+ as="headerEndpoint"
+ >
+ <gr-endpoint-decorator 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" 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
is="dom-repeat"
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
@@ -331,11 +377,32 @@
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]]">
+ <template
+ is="dom-repeat"
+ items="[[_dynamicPrependedContentEndpoints]]"
+ as="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]]">
+ </gr-endpoint-param>
+ <gr-endpoint-param name="path" value="[[file.__path]]">
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </template>
+ </template>
<!-- TODO: Remove data-url as it appears its not used -->
<span
data-url="[[_computeDiffURL(change, patchRange, file.__path, editMode)]]"
class="path"
+ role="gridcell"
>
<a
class="pathLink"
@@ -376,71 +443,126 @@
</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>
- <div class$="[[_computeSizeBarsClass(_showSizeBars, file.__path)]]">
- <svg width="61" height="8">
- <rect
- x$="[[_computeBarAdditionX(file, _sizeBarLayout)]]"
- y="0"
- height="8"
- fill="#388E3C"
- width$="[[_computeBarAdditionWidth(file, _sizeBarLayout)]]"
- ></rect>
- <rect
- x$="[[_computeBarDeletionX(_sizeBarLayout)]]"
- y="0"
- height="8"
- fill="#D32F2F"
- 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]]"
+ <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"
>
- +[[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>
+ <svg width="61" height="8">
+ <rect
+ x$="[[_computeBarAdditionX(file, _sizeBarLayout)]]"
+ y="0"
+ height="8"
+ fill="var(--positive-green-text-color)"
+ width$="[[_computeBarAdditionWidth(file, _sizeBarLayout)]]"
+ ></rect>
+ <rect
+ x$="[[_computeBarDeletionX(_sizeBarLayout)]]"
+ y="0"
+ height="8"
+ fill="var(--negative-red-text-color)"
+ width$="[[_computeBarDeletionWidth(file, _sizeBarLayout)]]"
+ ></rect>
+ </svg>
+ </div>
</div>
+ <div class="stats" role="gridcell">
+ <!-- The content must be in a separate div. It guarantees, that
+ gridcell always visible for screen readers.
+ For example, without a nested div screen readers pronounce the
+ "Commit message" row content with incorrect column headers.
+ -->
+ <div class$="[[_computeClass('', file.__path)]]">
+ <span
+ class="added"
+ tabindex="0"
+ aria-label$="[[file.lines_inserted]] lines added"
+ hidden$="[[file.binary]]"
+ >
+ +[[file.lines_inserted]]
+ </span>
+ <span
+ class="removed"
+ tabindex="0"
+ aria-label$="[[file.lines_deleted]] lines removed"
+ hidden$="[[file.binary]]"
+ >
+ -[[file.lines_deleted]]
+ </span>
+ <span
+ class$="[[_computeBinaryClass(file.size_delta)]]"
+ hidden$="[[!file.binary]]"
+ >
+ [[_formatBytes(file.size_delta)]] [[_formatPercentage(file.size,
+ file.size_delta)]]
+ </span>
+ </div>
+ </div>
+ <!-- endpoint: change-view-file-list-content -->
<template is="dom-if" if="[[_showDynamicColumns]]">
<template
is="dom-repeat"
items="[[_dynamicContentEndpoints]]"
as="contentEndpoint"
>
- <div class$="[[_computeClass('', file.__path)]]">
+ <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]]">
@@ -451,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)]]"
@@ -477,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
@@ -525,18 +673,19 @@
<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>
</div>
+ <!-- endpoint: change-view-file-list-summary -->
<template is="dom-if" if="[[_showDynamicColumns]]">
<template
is="dom-repeat"
@@ -544,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>
@@ -554,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.js b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
index 132cd16..c42c734 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
@@ -74,7 +74,9 @@
}
_computeGroups(includedIn, filterText) {
- if (!includedIn) { return []; }
+ if (!includedIn || filterText === undefined) {
+ return [];
+ }
const filter = item => !filterText.length ||
item.toLowerCase().indexOf(filterText.toLowerCase()) !== -1;
@@ -105,12 +107,6 @@
_computeLoadingClass(loaded) {
return loaded ? 'loading loaded' : 'loading';
}
-
- _onFilterChanged() {
- this.debounce('filter-change', () => {
- this._filterText = this.$.filterInput.bindValue;
- }, 100);
- }
}
customElements.define(GrIncludedInDialog.is, GrIncludedInDialog);
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 f5948e8..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,18 +67,21 @@
}
</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
>
</span>
- <iron-input placeholder="Filter" on-bind-value-changed="_onFilterChanged">
+ <iron-input
+ id="filterInput"
+ placeholder="Filter"
+ bind-value="{{_filterText}}"
+ >
<input
- id="filterInput"
is="iron-input"
placeholder="Filter"
- on-bind-value-changed="_onFilterChanged"
+ bind-value="{{_filterText}}"
/>
</iron-input>
</header>
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
index fe6119e..5d5b1fc 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
@@ -81,5 +81,20 @@
{title: 'Tags', items: ['v2.0', 'v2.1']},
]);
});
+
+ test('_computeGroups with .bindValue', done => {
+ element.$.filterInput.bindValue = 'stable-3.2';
+ const includedIn = {branches: [], tags: []};
+ includedIn.branches.push('master', 'stable-3.2');
+
+ setTimeout(() => {
+ const filterText = element._filterText;
+ assert.deepEqual(element._computeGroups(includedIn, filterText), [
+ {title: 'Branches', items: ['stable-3.2']},
+ ]);
+
+ done();
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
index 5b9730b..f5742ea 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
@@ -148,6 +148,13 @@
// Needed because when the selected item changes, it first changes to
// nothing and then to the new item.
if (!e.target.selectedItem) { return; }
+ for (const item of this.$.labelSelector.items) {
+ if (e.target.selectedItem === item) {
+ item.setAttribute('aria-checked', 'true');
+ } else {
+ item.removeAttribute('aria-checked');
+ }
+ }
this._selectedValueText = e.target.selectedItem.getAttribute('title');
// Needed to update the style of the selected button.
this.updateStyles();
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.js
index 77148ad..fb0f9e8 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.js
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.js
@@ -53,7 +53,6 @@
--button-background-color,
var(--table-header-background-color)
);
- color: var(--primary-text-color);
padding: 0 var(--spacing-m);
@apply --vote-chip-styles;
}
@@ -94,7 +93,9 @@
}
}
</style>
- <span class="labelNameCell">[[label.name]]</span>
+ <span class="labelNameCell" id="labelName" aria-hidden="true"
+ >[[label.name]]</span
+ >
<div class="buttonsCell">
<template
is="dom-repeat"
@@ -109,9 +110,12 @@
selected="[[_computeLabelValue(labels, permittedLabels, label)]]"
hidden$="[[!_computeAnyPermittedLabelValues(permittedLabels, label.name)]]"
on-selected-item-changed="_setSelectedValueText"
+ role="radiogroup"
+ aria-labelledby="labelName"
>
<template is="dom-repeat" items="[[_items]]" as="value">
<gr-button
+ role="radio"
vote$="[[_computeVoteAttribute(value, index, _items.length)]]"
has-tooltip=""
data-name$="[[label.name]]"
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
index 0fc6e4c..ca4fac3 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
@@ -104,6 +104,20 @@
sandbox.restore();
});
+ function checkAriaCheckedValid() {
+ const items = element.$.labelSelector.items;
+ const selectedItem = element.selectedItem;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ if (items[i] === selectedItem) {
+ assert.isTrue(item.hasAttribute('aria-checked'), `item ${i}`);
+ assert.equal(item.getAttribute('aria-checked'), 'true', `item ${i}`);
+ } else {
+ assert.isFalse(item.hasAttribute('aria-checked'), `item ${i}`);
+ }
+ }
+ }
+
test('label picker', () => {
const labelsChangedHandler = sandbox.stub();
element.addEventListener('labels-changed', labelsChangedHandler);
@@ -120,6 +134,7 @@
const detail = labelsChangedHandler.args[0][0].detail;
assert.equal(detail.name, 'Verified');
assert.equal(detail.value, '-1');
+ checkAriaCheckedValid();
});
test('_computeVoteAttribute', () => {
@@ -163,6 +178,7 @@
.textContent.trim(), '+1');
assert.strictEqual(
element.$.selectedValueLabel.textContent.trim(), 'good');
+ checkAriaCheckedValid();
});
test('do not display tooltips on touch devices', () => {
@@ -243,6 +259,7 @@
assert.strictEqual(selector.selected, ' 0');
assert.strictEqual(
element.$.selectedValueLabel.textContent.trim(), 'No score');
+ checkAriaCheckedValid();
});
test('without permitted labels', () => {
@@ -339,6 +356,7 @@
};
flushAsynchronousOperations();
assert.strictEqual(element.selectedValue, '-1');
+ checkAriaCheckedValid();
});
test('default_value is null if not permitted', () => {
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 b8a405a..e065008 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -74,14 +74,6 @@
computed: '_computeAuthor(message)',
},
/**
- * Used in cleaner_changelog experiment
- *
- * @type {Array} - array of threads attached to the message
- */
- commentThreads: {
- type: Array,
- },
- /**
* TODO(taoalpha): remove once the change log experiment is launched
*
* @type {Object} - a map on file and comments on it
@@ -140,12 +132,12 @@
type: String,
computed:
'_computeMessageContentCollapsed(message.message, message.tag,' +
- ' commentThreads)',
+ ' message.commentThreads)',
},
_commentCountText: {
type: Number,
- computed: '_computeCommentCountText(comments, commentThreads,'
- + ' _isCleanerLogExperimentEnabled)',
+ computed: '_computeCommentCountText(comments,'
+ + ' message.commentThreads.length, _isCleanerLogExperimentEnabled)',
},
_loggedIn: {
type: Boolean,
@@ -205,17 +197,16 @@
}
}
- _computeCommentCountText(comments, threads, isCleanerLogExperimentEnabled) {
+ _computeCommentCountText(
+ comments, threadsLength, isCleanerLogExperimentEnabled) {
// TODO(taoalpha): clean up after cleaner-changelog experiment launched
if (isCleanerLogExperimentEnabled) {
- if (!threads) return undefined;
- const count = threads.length;
- if (count === 0) {
+ if (threadsLength === 0) {
return undefined;
- } else if (count === 1) {
+ } else if (threadsLength === 1) {
return '1 comment';
} else {
- return `${count} comments`;
+ return `${threadsLength} comments`;
}
} else {
if (!comments) return undefined;
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 84713db..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
@@ -132,7 +132,7 @@
}
.score {
border-radius: var(--border-radius);
- color: var(--primary-text-color);
+ color: var(--vote-text-color);
display: inline-block;
padding: 0 var(--spacing-s);
text-align: center;
@@ -258,8 +258,8 @@
<template is="dom-if" if="[[_isCleanerLogExperimentEnabled]]">
<gr-thread-list
change="[[change]]"
- hidden$="[[!commentThreads.length]]"
- threads="[[commentThreads]]"
+ hidden$="[[!message.commentThreads.length]]"
+ threads="[[message.commentThreads]]"
change-num="[[changeNum]]"
logged-in="[[_loggedIn]]"
hide-toggle-buttons
@@ -280,7 +280,7 @@
items="[[update.reviewers]]"
as="reviewer"
>
- <gr-account-chip account="[[reviewer]]"> </gr-account-chip>
+ <gr-account-link account="[[reviewer]]"> </gr-account-link>
</template>
</div>
</template>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental.js
index ccc4a76..ae3a7d2 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental.js
@@ -275,8 +275,9 @@
* all messages and updates, aligns or massages some of the properties.
*/
_computeCombinedMessages(messages, reviewerUpdates, changeComments) {
- messages = messages || [];
- reviewerUpdates = reviewerUpdates || [];
+ const params = [messages, reviewerUpdates, changeComments];
+ if (params.some(o => o === undefined)) return [];
+
let mi = 0;
let ri = 0;
let combinedMessages = [];
@@ -312,6 +313,11 @@
m.commentThreads = computeThreads(m, combinedMessages, changeComments);
m._revision_number = computeRevision(m, combinedMessages);
m.tag = computeTag(m);
+ });
+ // computeIsImportant() depends on tags and revision numbers already being
+ // updated for all messages, so we have to compute this in its own forEach
+ // loop.
+ combinedMessages.forEach(m => {
m.isImportant = computeIsImportant(m, combinedMessages);
});
return combinedMessages;
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 83edab3..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
@@ -72,8 +72,10 @@
<paper-toggle-button
class="showAllActivityToggle"
checked="{{_showAllActivity}}"
+ aria-labelledby="showAllEntriesLabel"
+ role="switch"
></paper-toggle-button>
- <div>
+ <div id="showAllEntriesLabel">
<span>Show all entries</span>
<span class="hiddenEntries" hidden$="[[_showAllActivity]]">
([[_computeHiddenEntriesCount(_combinedMessages)]] hidden)
@@ -109,9 +111,9 @@
filter="_isMessageVisible"
>
<gr-message
+ change="[[change]]"
change-num="[[changeNum]]"
message="[[message]]"
- comment-threads="[[message.commentThreads]]"
project-name="[[projectName]]"
show-reply-button="[[showReplyButtons]]"
on-message-anchor-tap="_handleAnchorClick"
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_test.html
index 71f25b7..c520b05 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_test.html
@@ -302,11 +302,11 @@
const messageElements = getMessages();
// threads
assert.equal(
- messageElements[0].commentThreads.length,
+ messageElements[0].message.commentThreads.length,
3);
// first thread contains 1 comment
assert.equal(
- messageElements[0].commentThreads[0].comments.length,
+ messageElements[0].message.commentThreads[0].comments.length,
1);
});
@@ -420,6 +420,17 @@
assert.isFalse(TEST_ONLY.computeIsImportant(m3, [m1, m2, m3]));
});
+ test('isImportant is evaluated after tag update', () => {
+ const m1 = randomMessage(
+ {tag: MessageTag.TAG_NEW_PATCHSET, _revision_number: 1});
+ const m2 = randomMessage(
+ {tag: MessageTag.TAG_NEW_WIP_PATCHSET, _revision_number: 2});
+ element.messages = [m1, m2];
+ flushAsynchronousOperations();
+ assert.isFalse(m1.isImportant);
+ assert.isTrue(m2.isImportant);
+ });
+
test('messages without author do not throw', () => {
const messages = [{
_index: 5,
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js
index ddae150..5adfc53 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js
@@ -79,8 +79,10 @@
<paper-toggle-button
id="automatedMessageToggle"
checked="{{_hideAutomated}}"
+ aria-labelledby="onlyCommentsLabel"
+ role="switch"
></paper-toggle-button
- >Only comments
+ ><span id="onlyCommentsLabel">Only comments</span>
<span class="transparent separator"></span>
</span>
<gr-button
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 687dbd7..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 */
}
@@ -79,7 +76,7 @@
color: #1b5e20;
}
.submittableCheck {
- color: var(--vote-text-color-recommended);
+ color: var(--positive-green-text-color);
display: none;
}
.submittableCheck.submittable {
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 fe3bc67..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 */
@@ -643,11 +656,11 @@
}
_computeHideDraftList(draftCommentThreads) {
- return draftCommentThreads.length === 0;
+ return !draftCommentThreads || draftCommentThreads.length === 0;
}
_computeDraftsTitle(draftCommentThreads) {
- const total = draftCommentThreads.length;
+ const total = draftCommentThreads ? draftCommentThreads.length : 0;
if (total == 0) { return ''; }
if (total == 1) { return '1 Draft'; }
if (total > 1) { return total + ' Drafts'; }
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 f2455a2..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
@@ -74,47 +75,99 @@
};
}
- static get observers() { return ['_computeSortedThreads(threads.*)']; }
+ static get observers() {
+ return ['_updateSortedThreads(threads, threads.splices)'];
+ }
_computeShowDraftToggle(loggedIn) {
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:
- * - Unresolved threads with drafts (reverse chronological)
- * - Unresolved threads without drafts (reverse chronological)
- * - Resolved threads with drafts (reverse chronological)
- * - Resolved threads without drafts (reverse chronological)
+ * - 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 {!Object} changeRecord
+ * @param {Array<Object>} threads
+ * @param {!Object} spliceRecord
*/
- _computeSortedThreads(changeRecord) {
- const baseThreads = changeRecord.base;
- const threads = changeRecord.value;
- if (!baseThreads) { return []; }
- // TODO: should change how data flows to solve the root cause
+ _updateSortedThreads(threads, spliceRecord) {
+ if (!threads) {
+ this._sortedThreads = [];
+ return;
+ }
// We only want to sort on thread additions / removals to avoid
// re-rendering on modifications (add new reply / edit draft etc)
// https://polymer-library.polymer-project.org/3.0/docs/devguide/observers#array-observation
- let shouldSort = true;
- if (threads.indexSplices) {
- // Array splice mutations
- shouldSort = threads.indexSplices.addedCount !== 0
- || threads.indexSplices.removed.length;
- } else {
- // A replace mutation
- shouldSort = threads.length !== baseThreads.length;
- }
- this._updateSortedThreads(baseThreads, shouldSort);
- }
+ const isArrayMutation = spliceRecord &&
+ (spliceRecord.indexSplices.addedCount !== 0
+ || spliceRecord.indexSplices.removed.length);
- _updateSortedThreads(threads, shouldSort) {
if (this._sortedThreads
&& this._sortedThreads.length === threads.length
- && !shouldSort) {
+ && !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);
@@ -125,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 2195581..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,32 +509,29 @@
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', () => {
+ element.threads = undefined;
+ assert.deepEqual(element._sortedThreads, []);
});
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 = [];
@@ -376,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-main-header/gr-main-header_html.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js
index 19e833c..e50b408 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js
@@ -199,6 +199,7 @@
></gr-endpoint-decorator>
<gr-smart-search
id="search"
+ label="Search for changes"
search-query="{{searchQuery}}"
></gr-smart-search>
<gr-endpoint-decorator
@@ -221,6 +222,8 @@
class="settingsButton"
href$="[[_generateSettingsLink()]]"
title="Settings"
+ aria-label="Settings"
+ role="button"
>
<iron-icon icon="gr-icons:settings"></iron-icon>
</a>
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
index 02b4efd..39d00069 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -171,6 +171,14 @@
type: Number,
value: 1,
},
+ /**
+ * Invisible label for input element. This label is exposed to
+ * screen readers by nested element
+ */
+ label: {
+ type: String,
+ value: '',
+ },
};
}
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js
index e26f8a3..b0ef7af 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js
@@ -30,6 +30,7 @@
</style>
<form>
<gr-autocomplete
+ label="[[label]]"
show-search-icon=""
id="searchInput"
text="{{_inputVal}}"
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
index f6221c8..2d1212b 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
@@ -62,6 +62,14 @@
return this._fetchAccounts.bind(this);
},
},
+ /**
+ * Invisible label for input element. This label is exposed to
+ * screen readers by nested element
+ */
+ label: {
+ type: String,
+ value: '',
+ },
};
}
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js
index bb741ce..d490308 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js
@@ -20,6 +20,7 @@
<style include="shared-styles"></style>
<gr-search-bar
id="search"
+ label="[[label]]"
value="{{searchQuery}}"
on-handle-search="_handleSearch"
project-suggestions="[[_projectSuggestions]]"
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-patch-range-select/gr-patch-range-select_html.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.js
index 1d4b440..cc11cfa 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.js
@@ -51,7 +51,7 @@
}
}
</style>
- <span class="patchRange">
+ <span class="patchRange" aria-label="patch range starts with">
<gr-dropdown-list
id="basePatchDropdown"
value="[[basePatchNum]]"
@@ -67,8 +67,8 @@
>
</template>
</span>
- <span class="arrow">→</span>
- <span class="patchRange">
+ <span aria-hidden="true" class="arrow">→</span>
+ <span class="patchRange" aria-label="patch range ends with">
<gr-dropdown-list
id="patchNumDropdown"
value="[[patchNum]]"
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 ad47d52..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>
@@ -111,6 +112,7 @@
<main>
<gr-smart-search
id="search"
+ label="Search for changes"
search-query="{{params.query}}"
hidden="[[!mobileSearch]]"
>
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.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index e6a809e..4cf56a7 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -184,6 +184,14 @@
type: Boolean,
value: false,
},
+ /**
+ * Invisible label for input element. This label is exposed to
+ * screen readers by paper-input
+ */
+ label: {
+ type: String,
+ value: '',
+ },
/** The DOM element of the selected suggestion. */
_selected: Object,
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js
index eae8741..c0e8abf 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js
@@ -66,6 +66,12 @@
height: 0;
display: none;
}
+ /* Hide label for input. The label is still visible for
+ screen readers. Workaround found at:
+ https://github.com/PolymerElements/paper-input/issues/478 */
+ --paper-input-container-label: {
+ display: none;
+ }
}
paper-input.warnUncommitted {
--paper-input-container-input: {
@@ -85,6 +91,7 @@
on-focus="_onInputFocus"
on-blur="_onInputBlur"
autocomplete="off"
+ label="[[label]]"
>
<!-- prefix as attribute is required to for polymer 1 -->
<div slot="prefix" prefix="">
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 6ef4807..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';
/**
@@ -65,6 +65,11 @@
value: false,
reflectToAttribute: true,
},
+ ariaDisabled: {
+ type: Boolean,
+ computed: '_computeDisabled(disabled, loading)',
+ reflectToAttribute: true,
+ },
_disabled: {
type: Boolean,
@@ -108,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-change-star/gr-change-star.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
index 886f0c1..dac1755 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
@@ -53,6 +53,10 @@
return `gr-icons:star${starred ? '' : '-border'}`;
}
+ _computeAriaLabel(starred) {
+ return starred ? 'Unstar this change' : 'Star this change';
+ }
+
toggleStar() {
const newVal = !this.change.starred;
this.set('change.starred', newVal);
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.js
index f723717a..3b7b1af 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.js
@@ -37,7 +37,11 @@
);
}
</style>
- <button aria-label="Change star" on-click="toggleStar">
+ <button
+ role="checkbox"
+ aria-label="[[_computeAriaLabel(change.starred)]]]"
+ on-click="toggleStar"
+ >
<iron-icon
class$="[[_computeStarClass(change.starred)]]"
icon$="[[_computeStarIcon(change.starred)]]"
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.js
index 904ef1d..38e620f 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.js
@@ -25,43 +25,43 @@
white-space: nowrap;
}
:host(.merged) .chip {
- background-color: #5b9d52;
- color: #5b9d52;
+ background-color: var(--status-merged);
+ color: var(--status-merged);
}
:host(.abandoned) .chip {
- background-color: #afafaf;
- color: #afafaf;
+ background-color: var(--status-abandoned);
+ color: var(--status-abandoned);
}
:host(.wip) .chip {
- background-color: #8f756c;
- color: #8f756c;
+ background-color: var(--status-wip);
+ color: var(--status-wip);
}
:host(.private) .chip {
- background-color: #c17ccf;
- color: #c17ccf;
+ background-color: var(--status-private);
+ color: var(--status-private);
}
:host(.merge-conflict) .chip {
- background-color: #dc5c60;
- color: #dc5c60;
+ background-color: var(--status-conflict);
+ color: var(--status-conflict);
}
:host(.active) .chip {
- background-color: #29b6f6;
- color: #29b6f6;
+ background-color: var(--status-active);
+ color: var(--status-active);
}
:host(.ready-to-submit) .chip {
- background-color: #e10ca3;
- color: #e10ca3;
+ background-color: var(--status-ready);
+ color: var(--status-ready);
}
:host(.custom) .chip {
- background-color: #825cc2;
- color: #825cc2;
+ background-color: var(--status-custom);
+ color: var(--status-custom);
}
:host([flat]) .chip {
background-color: transparent;
padding: 0;
}
:host(:not([flat])) .chip {
- color: white;
+ color: var(--status-text-color);
}
</style>
<gr-tooltip-content
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 b83cdf2..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,13 +273,17 @@
>
<iron-icon id="icon" icon="gr-icons:delete"></iron-icon>
</gr-button>
- <span class="date" on-click="_handleAnchorClick">
+ <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=""
date-str="[[comment.updated]]"
></gr-date-formatter>
</span>
- <div class="show-hide">
+ <div class="show-hide" tabindex="0">
<label
class="show-hide"
aria-label="[[_computeShowHideAriaLabel(collapsed)]]"
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 bcb85e2..070ac02 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
@@ -100,6 +100,10 @@
<g id="attention"><path d="M5.5 19 l9 0 c.67 0 1.27 -.33 1.63 -.84 L20.5 12 l-4.37 -6.16 c-.36 -.51 -.96 -.84 -1.63 -.84 l-9 0 L9 12 z"></path></g>
<!-- This SVG is a copy from material.io https://material.io/icons/#pets-->
<g id="pets"><circle cx="4.5" cy="9.5" r="2.5"/><circle cx="9" cy="5.5" r="2.5"/><circle cx="15" cy="5.5" r="2.5"/><circle cx="19.5" cy="9.5" r="2.5"/><path d="M17.34 14.86c-.87-1.02-1.6-1.89-2.48-2.91-.46-.54-1.05-1.08-1.75-1.32-.11-.04-.22-.07-.33-.09-.25-.04-.52-.04-.78-.04s-.53 0-.79.05c-.11.02-.22.05-.33.09-.7.24-1.28.78-1.75 1.32-.87 1.02-1.6 1.89-2.48 2.91-1.31 1.31-2.92 2.76-2.62 4.79.29 1.02 1.02 2.03 2.33 2.32.73.15 3.06-.44 5.54-.44h.18c2.48 0 4.81.58 5.54.44 1.31-.29 2.04-1.31 2.33-2.32.31-2.04-1.3-3.49-2.61-4.8z"/><path d="M0 0h24v24H0z" fill="none"/></g>
+ <!-- This SVG is a copy from material.io https://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 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-label-info/gr-label-info.js b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
index c003712..eb23708 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
@@ -168,6 +168,11 @@
* order to trigger computation when a label is removed from the change.
*/
_computeShowPlaceholder(labelInfo, changeLabelsRecord) {
+ if (labelInfo &&
+ !labelInfo.values && (labelInfo.rejected || labelInfo.approved)) {
+ return 'hidden';
+ }
+
if (labelInfo && labelInfo.all) {
for (const label of labelInfo.all) {
if (label.value && label.value != labelInfo.default_value) {
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
index 94cfaea..b97b5b3 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
@@ -232,6 +232,18 @@
element.labelInfo = {all: [{value: 1}]};
assert.isTrue(isHidden(element.shadowRoot
.querySelector('.placeholder')));
+ element.labelInfo = {rejected: []};
+ assert.isTrue(isHidden(element.shadowRoot
+ .querySelector('.placeholder')));
+ element.labelInfo = {values: [], rejected: [], all: [{value: 1}]};
+ assert.isTrue(isHidden(element.shadowRoot
+ .querySelector('.placeholder')));
+ element.labelInfo = {approved: []};
+ assert.isTrue(isHidden(element.shadowRoot
+ .querySelector('.placeholder')));
+ element.labelInfo = {values: [], approved: [], all: [{value: 1}]};
+ assert.isTrue(isHidden(element.shadowRoot
+ .querySelector('.placeholder')));
});
});
</script>
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/bind-parameters.js b/polygerrit-ui/app/samples/bind-parameters.js
index 8f08e27..2c89064 100644
--- a/polygerrit-ui/app/samples/bind-parameters.js
+++ b/polygerrit-ui/app/samples/bind-parameters.js
@@ -14,9 +14,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-const {Element, html} = Polymer;
-class MyBindSample extends Element {
+// Element class exists in all browsers:
+// https://developer.mozilla.org/en-US/docs/Web/API/Element
+// Rename it to PolymerElement to avoid conflicts. Also,
+// typescript reports the following error:
+// error TS2451: Cannot redeclare block-scoped variable 'Element'.
+const {html, Element: PolymerElement} = Polymer;
+
+class MyBindSample extends PolymerElement {
static get is() { return 'my-bind-sample'; }
static get properties() {
@@ -62,4 +68,4 @@
// between the file list and the change log
plugin.registerCustomComponent(
'change-view-integration', 'my-bind-sample');
-});
\ No newline at end of file
+});
diff --git a/polygerrit-ui/app/samples/extra-column-on-file-list.js b/polygerrit-ui/app/samples/extra-column-on-file-list.js
new file mode 100644
index 0000000..2e37c01
--- /dev/null
+++ b/polygerrit-ui/app/samples/extra-column-on-file-list.js
@@ -0,0 +1,77 @@
+/**
+ * @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 an extra column to file list on change page to show
+ * the first character of the path.
+ */
+
+// Header of this extra column
+class ColumnHeader extends Polymer.Element {
+ static get is() { return 'column-header'; }
+
+ static get template() {
+ return Polymer.html`
+ <style>
+ :host {
+ display: block;
+ padding-right: var(--spacing-m);
+ min-width: 5em;
+ }
+ </style>
+ <div>First Char</div>
+ `;
+ }
+}
+
+customElements.define(ColumnHeader.is, ColumnHeader);
+
+// Content of this extra column
+class ColumnContent extends Polymer.Element {
+ static get is() { return 'column-content'; }
+
+ static get properties() {
+ return {
+ path: String,
+ };
+ }
+
+ static get template() {
+ return Polymer.html`
+ <style>
+ :host {
+ display:block;
+ padding-right: var(--spacing-m);
+ min-width: 5em;
+ }
+ </style>
+ <div>[[getStatus(path)]]</div>
+ `;
+ }
+
+ getStatus(path) {
+ return path.charAt(0);
+ }
+}
+
+customElements.define(ColumnContent.is, ColumnContent);
+
+Gerrit.install(plugin => {
+ plugin.registerDynamicCustomComponent(
+ 'change-view-file-list-header-prepend', ColumnHeader.is);
+ plugin.registerDynamicCustomComponent(
+ 'change-view-file-list-content-prepend', ColumnContent.is);
+});
\ 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 00f95f5..5aaea30 100644
--- a/polygerrit-ui/app/samples/repo-command.js
+++ b/polygerrit-ui/app/samples/repo-command.js
@@ -14,9 +14,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-const {Element, html} = Polymer;
-class RepoCommandLow extends Element {
+// Element class exists in all browsers:
+// https://developer.mozilla.org/en-US/docs/Web/API/Element
+// Rename it to PolymerElement to avoid conflicts. Also,
+// typescript reports the following error:
+// error TS2451: Cannot redeclare block-scoped variable 'Element'.
+const {html, Element: PolymerElement} = Polymer;
+
+class RepoCommandLow extends PolymerElement {
static get is() { return 'repo-command-low'; }
static get properties() {
@@ -27,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() {
@@ -70,4 +84,4 @@
// Low-level API
plugin.registerCustomComponent(
'repo-command', 'repo-command-low');
-});
\ No newline at end of file
+});
diff --git a/polygerrit-ui/app/samples/some-screen.js b/polygerrit-ui/app/samples/some-screen.js
index 09acc81..c600fe4 100644
--- a/polygerrit-ui/app/samples/some-screen.js
+++ b/polygerrit-ui/app/samples/some-screen.js
@@ -14,9 +14,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-const {Element, html} = Polymer;
-class SomeScreenMain extends Element {
+// Element class exists in all browsers:
+// https://developer.mozilla.org/en-US/docs/Web/API/Element
+// Rename it to PolymerElement to avoid conflicts. Also,
+// typescript reports the following error:
+// error TS2451: Cannot redeclare block-scoped variable 'Element'.
+const {html, Element: PolymerElement} = Polymer;
+
+class SomeScreenMain extends PolymerElement {
static get is() { return 'some-screen-main'; }
static get properties() {
@@ -64,4 +70,4 @@
plugin.hook('change-metadata-item').onAttached(el => {
el.innerHTML = `<a href="${mainUrl}">Plugin screen</a>`;
});
-});
\ No newline at end of file
+});
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/gr-voting-styles.js b/polygerrit-ui/app/styles/gr-voting-styles.js
index 4860428..60bf623 100644
--- a/polygerrit-ui/app/styles/gr-voting-styles.js
+++ b/polygerrit-ui/app/styles/gr-voting-styles.js
@@ -26,6 +26,7 @@
box-shadow: none;
box-sizing: border-box;
min-width: 3em;
+ color: var(--vote-text-color);
}
}
</style>
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/styles/themes/app-theme.js b/polygerrit-ui/app/styles/themes/app-theme.js
index 5d5d9e3..718d6a5 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.js
+++ b/polygerrit-ui/app/styles/themes/app-theme.js
@@ -38,10 +38,12 @@
--primary-button-text-color: white;
/* Used on text color for change list that doesn't need user's attention. */
--reviewed-text-color: black;
+ --vote-text-color: black;
+ --status-text-color: white;
--tooltip-text-color: white;
- --vote-text-color-recommended: #388e3c;
- --vote-text-color-disliked: #d32f2f;
-
+ --negative-red-text-color: #d93025;
+ --positive-green-text-color: #188038;
+
/* background colors */
/* primary background colors */
--background-color-primary: #ffffff;
@@ -83,6 +85,16 @@
--border-color: #e8e8e8;
--comment-separator-color: #dadce0;
+ /* status colors */
+ --status-merged: #188038;
+ --status-abandoned: #5f6368;
+ --status-wip: #795548;
+ --status-private: #a142f4;
+ --status-conflict: #d93025;
+ --status-active: #1976d2;
+ --status-ready: #b80672;
+ --status-custom: #681da8;
+
/* fonts */
--font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
--header-font-family: 'Open Sans', 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.html b/polygerrit-ui/app/styles/themes/dark-theme.html
index 4248878..18b2fd6 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.html
+++ b/polygerrit-ui/app/styles/themes/dark-theme.html
@@ -33,12 +33,14 @@
--deemphasized-text-color: #9aa0a6;
--default-button-text-color: #8ab4f8;
--error-text-color: red;
- --primary-button-text-color: var(--primary-text-color);
+ --primary-button-text-color: black;
/* Used on text color for change list doesn't need user's attention. */
--reviewed-text-color: #dadce0;
+ --vote-text-color: black;
+ --status-text-color: black;
--tooltip-text-color: white;
- --vote-text-color-recommended: #388e3c;
- --vote-text-color-disliked: #d32f2f;
+ --negative-red-text-color: #f28b82;
+ --positive-green-text-color: #81c995;
/* background colors */
/* primary background colors */
@@ -71,6 +73,16 @@
--border-color: #5f6368;
--comment-separator-color: var(--border-color);
+ /* status colors */
+ --status-merged: #5bb974;
+ --status-abandoned: #dadce0;
+ --status-wip: #bcaaa4;
+ --status-private: #d7aefb;
+ --status-conflict: #f28b82;
+ --status-active: #669df6;
+ --status-ready: #f439a0;
+ --status-custom: #af5cf7;
+
/* fonts */
--font-weight-bold: 700; /* 700 is the same as 'bold' */
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/polygerrit-ui/server.go b/polygerrit-ui/server.go
index 120aff5..c44493d 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -61,6 +61,7 @@
dirListingMux := http.NewServeMux()
dirListingMux.Handle("/styles/", http.StripPrefix("/styles/", http.FileServer(http.Dir("app/styles"))))
+ dirListingMux.Handle("/samples/", http.StripPrefix("/samples/", http.FileServer(http.Dir("app/samples"))))
dirListingMux.Handle("/elements/", http.StripPrefix("/elements/", http.FileServer(http.Dir("app/elements"))))
dirListingMux.Handle("/behaviors/", http.StripPrefix("/behaviors/", http.FileServer(http.Dir("app/behaviors"))))
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/BUILD b/tools/bzl/BUILD
index a0f5bd1..7febbac 100644
--- a/tools/bzl/BUILD
+++ b/tools/bzl/BUILD
@@ -3,3 +3,8 @@
"test_empty.sh",
"test_license.sh",
])
+
+sh_test(
+ name = "always_pass_test",
+ srcs = ["always_pass_test.sh"],
+)
diff --git a/tools/bzl/always_pass_test.sh b/tools/bzl/always_pass_test.sh
new file mode 100755
index 0000000..15c58ca
--- /dev/null
+++ b/tools/bzl/always_pass_test.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+#
+# 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 is a dummy test to put on the command line to avoid no tests
+# found outcome in `bazel test` command. See this upstream issue:
+# https://github.com/bazelbuild/bazel/issues/11465
+
+exit 0
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 3cffb79..d49e700 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -1,5 +1,6 @@
load("@rules_java//java:defs.bzl", "java_binary", "java_library")
load("//tools/bzl:genrule2.bzl", "genrule2")
+load("//:version.bzl", "GERRIT_VERSION")
PLUGIN_DEPS = ["//plugins:plugin-lib"]
@@ -39,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,
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/node_tools/package.json b/tools/node_tools/package.json
index 2af06b4..6fafe63 100644
--- a/tools/node_tools/package.json
+++ b/tools/node_tools/package.json
@@ -3,8 +3,8 @@
"description": "Gerrit Build Tools",
"browser": false,
"dependencies": {
- "@bazel/rollup": "^0.41.0",
- "@bazel/typescript": "^1.0.1",
+ "@bazel/rollup": "^1.6.1",
+ "@bazel/typescript": "^1.6.1",
"@types/node": "^10.17.12",
"@types/parse5": "^4.0.0",
"@types/parse5-html-rewriting-stream": "^5.1.2",
@@ -16,7 +16,7 @@
"rollup": "^1.27.5",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^5.1.3",
- "typescript": "^3.7.4"
+ "typescript": "3.8.2"
},
"devDependencies": {},
"license": "Apache-2.0",
diff --git a/tools/node_tools/yarn.lock b/tools/node_tools/yarn.lock
index 0648c8d..78349fa 100644
--- a/tools/node_tools/yarn.lock
+++ b/tools/node_tools/yarn.lock
@@ -492,15 +492,15 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
-"@bazel/rollup@^0.41.0":
- version "0.41.0"
- resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-0.41.0.tgz#8dfaccc239f3efbae1c816b0ce2aeb6069d23582"
- integrity sha512-M+ybGfcxTXnAS1QiaijLEfUznNYLA0cqeGXnYHSRrOhq2U7yesfavxbBtfLSKtg32ktmlHts5te8Zg82BS4DPQ==
+"@bazel/rollup@^1.6.1":
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-1.6.1.tgz#7ec9d39a3fca23256fca55410339724804802616"
+ integrity sha512-FhblJkpd8VKl9txhAAIotSsIOHRpPd2FgJG7Op3uV7LfaCVBmUs3XDBZCgfwt5wmEpd3lwCHA1Ei+O/URS2+5w==
-"@bazel/typescript@^1.0.1":
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-1.1.0.tgz#b57ac6c6d627577f394a60fb540fbbdf53bcff0d"
- integrity sha512-QnTdb6rwZUR+KfUuAdyazpkA7BOvrWRe7tkPDdyIZHJdBPYdpJW+AapnFSfxvXEIP0Nwesl5KP6Saau0GPiBLg==
+"@bazel/typescript@^1.6.1":
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-1.6.1.tgz#1bf83c20021d359bc9b532181981ac540584a30c"
+ integrity sha512-wQ9AASRcG1jLQOpJfNOMjZzPpwIV/9qTOxCFvp55ga6A5a2qveQr8JJ7jHHbBM0LtK+slEPixXmVmtEOwfKsIg==
dependencies:
protobufjs "6.8.8"
semver "5.6.0"
@@ -949,11 +949,16 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.118.tgz#8014a9b1dee0b72b4d7cd142563f1af21241c3a2"
integrity sha512-N33cKXGSqhOYaPiT4xUGsYlPPDwFtQM/6QxJxuMXA/7BcySW+lkn2yigWP7vfs4daiL/7NJNU6DMCqg5N4B+xQ==
-"@types/node@^10.1.0", "@types/node@^10.17.12":
+"@types/node@^10.1.0":
version "10.17.13"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c"
integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg==
+"@types/node@^10.17.12":
+ version "10.17.24"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.24.tgz#c57511e3a19c4b5e9692bb2995c40a3a52167944"
+ integrity sha512-5SCfvCxV74kzR3uWgTYiGxrd69TbT1I6+cMx1A5kEly/IVveJBimtAMlXiEyVFn5DvUFewQWxOOiJhlxeQwxgA==
+
"@types/node@^4.0.30":
version "4.9.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-4.9.4.tgz#75ef91733afaa856b01e12da6ecf48aa9d5e221f"
@@ -7871,10 +7876,10 @@
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
-typescript@^3.7.4:
- version "3.7.5"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae"
- integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==
+typescript@3.8.2:
+ version "3.8.2"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.2.tgz#91d6868aaead7da74f493c553aeff76c0c0b1d5a"
+ integrity sha512-EgOVgL/4xfVrCMbhYKUQTdF37SQn4Iw73H5BgCrF1Abdun7Kwy/QZsE/ssAy0y4LxBbvua3PIbFsbRczWWnDdQ==
typical@^2.6.0, typical@^2.6.1:
version "2.6.1"
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index eced448..1da5004 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -23,8 +23,8 @@
maven_jar(
name = "dropwizard-core",
- artifact = "io.dropwizard.metrics:metrics-core:4.1.8",
- sha1 = "f4765fc53af5d5712261a7e29afe97beb6b30118",
+ artifact = "io.dropwizard.metrics:metrics-core:4.1.9",
+ sha1 = "dd76a62b007ffea9e6aba10f64c04173ef65f895",
)
SSHD_VERS = "2.4.0"
@@ -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(
diff --git a/yarn.lock b/yarn.lock
index 8379dd7..02ac32e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -485,15 +485,15 @@
lodash "^4.17.11"
to-fast-properties "^2.0.0"
-"@bazel/rollup@^1.1.0":
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-1.2.0.tgz#8b9569ed6f1c00d2a833567901f8ee4600a389fb"
- integrity sha512-yrXW+AAUoqc9qN/CweD5p8OEN9bNKFjXnXPBRE4w84LxpkmaJFx+yQJ++c1F57zWMoq2o9EV4CM7y+mK8zxwUg==
+"@bazel/rollup@^1.6.1":
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-1.6.1.tgz#7ec9d39a3fca23256fca55410339724804802616"
+ integrity sha512-FhblJkpd8VKl9txhAAIotSsIOHRpPd2FgJG7Op3uV7LfaCVBmUs3XDBZCgfwt5wmEpd3lwCHA1Ei+O/URS2+5w==
-"@bazel/typescript@^1.0.1":
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-1.2.0.tgz#ab2016e1d6eb7a86b44536e887f51eaf3d75f1a7"
- integrity sha512-hPEG8K0psyEcs6HFRiqZNQwXL/dQ8sXKdrNFWv87+rh+YUNfd58uktoynhllympOPThcbUZcZicLWBEFQOc8nA==
+"@bazel/typescript@^1.6.1":
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-1.6.1.tgz#1bf83c20021d359bc9b532181981ac540584a30c"
+ integrity sha512-wQ9AASRcG1jLQOpJfNOMjZzPpwIV/9qTOxCFvp55ga6A5a2qveQr8JJ7jHHbBM0LtK+slEPixXmVmtEOwfKsIg==
dependencies:
protobufjs "6.8.8"
semver "5.6.0"
@@ -907,9 +907,9 @@
integrity sha512-rp7La3m845mSESCgsJePNL/JQyhkOJA6G4vcwvVgkDAwHhGdq5GCumxmPjEk1MZf+8p5ZQAUE7tqgQRQTXN7uQ==
"@types/node@^10.1.0":
- version "10.17.13"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c"
- integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg==
+ version "10.17.24"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.24.tgz#c57511e3a19c4b5e9692bb2995c40a3a52167944"
+ integrity sha512-5SCfvCxV74kzR3uWgTYiGxrd69TbT1I6+cMx1A5kEly/IVveJBimtAMlXiEyVFn5DvUFewQWxOOiJhlxeQwxgA==
"@types/node@^4.0.30":
version "4.9.3"
@@ -8596,7 +8596,12 @@
source-map "^0.5.6"
source-map-support "^0.4.2"
-tslib@^1.8.1, tslib@^1.9.0:
+tslib@^1.8.1:
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
+ integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
+
+tslib@^1.9.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
@@ -8657,16 +8662,16 @@
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
+typescript@3.8.2:
+ version "3.8.2"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.2.tgz#91d6868aaead7da74f493c553aeff76c0c0b1d5a"
+ integrity sha512-EgOVgL/4xfVrCMbhYKUQTdF37SQn4Iw73H5BgCrF1Abdun7Kwy/QZsE/ssAy0y4LxBbvua3PIbFsbRczWWnDdQ==
+
typescript@^2.4.1:
version "2.9.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==
-typescript@^3.7.4:
- version "3.7.5"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae"
- integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==
-
typical@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d"